├── .appcast.xml ├── .babelrc ├── .editorconfig ├── .eslintrc.yml ├── .gitignore ├── .nvmrc ├── CHANGELOG.md ├── README.md ├── docs ├── demo.gif └── sketch-templates │ ├── SanFranciscoTypeface.sketch │ └── TypesettingsStarter.sketch ├── package.json ├── src ├── assets │ └── icons │ │ ├── characterSpacingRunner.png │ │ ├── lineHeightRunner.png │ │ ├── mark.png │ │ └── registerRunner.png ├── directory │ ├── SFCompactText.json │ ├── SFProDisplay.json │ └── SFProText.json ├── plugin │ ├── Typesetter.js │ ├── commands │ │ ├── auto.js │ │ ├── preferences.js │ │ ├── register.js │ │ └── typeset.js │ ├── manifest.json │ ├── storage │ │ └── index.js │ └── utils │ │ ├── fonts.js │ │ └── helpers.js └── webview │ ├── client.js │ ├── components │ ├── Button │ │ └── index.jsx │ ├── Checkbox │ │ ├── index.jsx │ │ └── styles.js │ ├── Field │ │ └── index.jsx │ ├── Page │ │ ├── Footer │ │ │ ├── index.jsx │ │ │ └── mark.svg │ │ ├── Header │ │ │ └── index.jsx │ │ └── index.jsx │ └── TextField │ │ ├── index.jsx │ │ └── styles.js │ ├── index.html │ ├── pages │ ├── SettingsPage │ │ ├── Section.jsx │ │ └── index.jsx │ └── index.js │ └── style │ ├── globals │ └── index.js │ └── variables │ ├── index.js │ └── typesettings.js └── webpack.skpm.config.js /.appcast.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | -------------------------------------------------------------------------------- /.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "plugins": [ 3 | "@emotion/babel-plugin-core", 4 | "@babel/plugin-proposal-class-properties" 5 | ] 6 | } 7 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*] 4 | indent_style = space 5 | indent_size = 2 6 | trim_trailing_whitespace = true 7 | insert_final_newline = true 8 | 9 | [*.md] 10 | trim_trailing_whitespace = false 11 | -------------------------------------------------------------------------------- /.eslintrc.yml: -------------------------------------------------------------------------------- 1 | --- 2 | root: true 3 | 4 | extends: 5 | - sketch 6 | - airbnb 7 | - plugin:import/errors 8 | 9 | globals: 10 | CGColorCreateGenericRGB: true 11 | CGRectGetMaxY: true 12 | COSAlertWindow: true 13 | NSOffState: true 14 | NSOnState: true 15 | NSSwitchButton: true 16 | NSHomeDirectory: true 17 | NSStringPboardType: true 18 | document: true 19 | window: true 20 | 21 | plugins: 22 | - react 23 | 24 | parser: babel-eslint 25 | 26 | settings: 27 | import/resolver: 28 | webpack: 29 | config: 'webpack.skpm.config.js' 30 | node: 31 | paths: 32 | - src 33 | import/core-modules: 34 | - sketch/ui 35 | - sketch/dom 36 | - sketch/settings 37 | 38 | rules: 39 | import/prefer-default-export: off 40 | no-param-reassign: off 41 | no-console: off 42 | arrow-parens: 43 | - error 44 | - as-needed 45 | - requireForBlockBody: true 46 | object-curly-spacing: 47 | - error 48 | - always 49 | array-bracket-spacing: 50 | - error 51 | - always 52 | template-curly-spacing: 53 | - error 54 | - always 55 | semi: 56 | - error 57 | - never 58 | comma-dangle: 59 | - error 60 | - never 61 | keyword-spacing: 62 | - error 63 | - before: true 64 | after: true 65 | overrides: 66 | return: 67 | after: true 68 | throw: 69 | after: true 70 | case: 71 | after: true 72 | react/jsx-curly-spacing: 73 | - error 74 | - when: always 75 | attributes: 76 | allowMultiline: true 77 | children: true -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | lib 2 | dist 3 | build 4 | node_modules 5 | .npm 6 | *.log 7 | *.lock 8 | .DS_Store 9 | typesettings.sketchplugin 10 | src/directory/PostmatesStd.json 11 | -------------------------------------------------------------------------------- /.nvmrc: -------------------------------------------------------------------------------- 1 | v10.8.0 2 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | ## v0.2.0 (2018-09-03) 2 | 3 | #### :rocket: Enhancement 4 | * [#6](https://github.com/buames/typesettings-sketch-plugin/issues/6) Plugin preferences moved to WebView 5 | * [#9](https://github.com/buames/typesettings-sketch-plugin/issues/9) Improve managing local directory paths 6 | * [#11](https://github.com/buames/typesettings-sketch-plugin/issues/11) New props add to typesettings json. Allows for use with [typesettings-js](https://github.com/buames/typesettings-js) 7 | 8 | #### :bug: Bug Fix 9 | * [#13](https://github.com/buames/typesettings-sketch-plugin/issues/13) Paragraph spacing isn't appling when setting line height 10 | 11 | #### :house: Internal 12 | * [#7](https://github.com/buames/typesettings-sketch-plugin/issues/7) Add skpm to deps 13 | * [#8](https://github.com/buames/typesettings-sketch-plugin/issues/8) Linting for webviews 14 | * [#10](https://github.com/buames/typesettings-sketch-plugin/issues/10) Move dialogs to use @skpm/dialog 15 | 16 | ## v0.1.1 (2018-07-04) 17 | 18 | #### :rocket: Enhancement 19 | * [#4](https://github.com/buames/typesettings-sketch-plugin/pull/4) Improved workflow and Sketch Runner support 20 | 21 | #### :house: Internal 22 | * [#3](https://github.com/buames/typesettings-sketch-plugin/pull/3) Add linting and changelog 23 | 24 | ## v0.1.0 (2018-06-29) 25 | 26 | #### :rocket: Enhancement 27 | * [#2](https://github.com/buames/typesettings-sketch-plugin/pull/2) Choose to Merge or Replace typesettings that exists 28 | * [#2](https://github.com/buames/typesettings-sketch-plugin/pull/2) Ignore text layers with a prefix or suffix 29 | * [#2](https://github.com/buames/typesettings-sketch-plugin/pull/2) Add ability to set ignore prefix and suffix to the plugin preferences 30 | 31 | #### :memo: Documentation 32 | * [#2](https://github.com/buames/typesettings-sketch-plugin/pull/2) Add an icon 33 | * [#2](https://github.com/buames/typesettings-sketch-plugin/pull/2) Add SF typeface sketch file to use for registering 34 | 35 | #### :house: Internal 36 | * [#2](https://github.com/buames/typesettings-sketch-plugin/pull/2) Add version checking and backwards compatibility 37 | * [#2](https://github.com/buames/typesettings-sketch-plugin/pull/2) Updated directory json to latest version of plugin 38 | * [#2](https://github.com/buames/typesettings-sketch-plugin/pull/2) Code refactor and clean up 39 | 40 | ## v0.0.2 (2018-06-24) 41 | 42 | #### :rocket: Enhancement 43 | * [#1](https://github.com/buames/typesettings-sketch-plugin/pull/1) Add San Francisco Typesettings 44 | * [#1](https://github.com/buames/typesettings-sketch-plugin/pull/1) Add SF Compact Typesettings 45 | 46 | #### :bug: Bug Fix 47 | * [#1](https://github.com/buames/typesettings-sketch-plugin/pull/1) Fix issue where fonts are added to wrong family when multple families are selected 48 | 49 | ## v0.0.1 (2018-06-24) 50 | 51 | * Initial Release 52 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Typesettings Sketch Plugin 2 | 3 | Create typesettings once and use them anywhere. Set character spacing and line height of text layers based on the `font family`, `font weight`, `font size`, and `letter casing`. Better yet, let the plugin do it for you automatically when you allow it to from the plugin preferences. 4 | 5 | [Below](#pre-registered) you'll find typsettings that are already included with the plugin. 6 | 7 | ## Demo 8 | 9 | ![Demo](/docs/demo.gif) 10 | 11 | Get the [TypesettingsStarter.sketch](/docs/sketch-templates/TypesettingsStarter.sketch) file to get going easily. 12 | 13 | ## Installation 14 | 15 | You can install the plugin with [Sketch Runner](https://sketchrunner.com) or by downloading it from the [latest release](https://github.com/buames/typesettings-sketch-plugin/releases/latest). 16 | 17 | ## Commands 18 | 19 | ### Set Character Spacing 20 | 21 | ``` 22 | cmd + shift + right arrow 23 | ``` 24 | 25 | Sets the character spacing of any selected text layer based on the font and text size. You can `enable automatic character spacing` in the plugin preferences. 26 | 27 | ### Set Line Height 28 | 29 | ``` 30 | cmd + shift + down arrow 31 | ``` 32 | 33 | Sets the line height of any selected text layer based on the font and text size. You can `enable automatic line height` in the plugin preferences. 34 | 35 | ### Register Typesettings 36 | 37 | ``` 38 | ctrl + alt + cmd + return 39 | ``` 40 | 41 | To register typesettings, you'll want to do the following: 42 | 43 | 1. Create your text layers using any size, weight, and letter casing permutation 44 | 2. Set the character spacing and line height for each 45 | 2. Select all text layers 46 | 3. Go to `Plugins` > `Typesettings` > `Register Typesettings` 47 | 48 | **Letter Casing** 49 | 50 | You can create typesettings for uppercase, lowercase (if that's a thing?), and no casing. 51 | 52 | In order to properly save the typesettings for a given letter casing, you'll need to make sure that the text layers has the desired `Text Transform` option when registering the typesettings. 53 | 54 | For example: 55 | 1. Create two text layers using the same font family, font size, and weight 56 | 2. Select one of the text layers and set the `Text Transform` option to `Uppercase` using the [Text Inspector](https://sketchapp.com/docs/text/text-inspector) 57 | 3. Set the character spacing and line height of each text layer to whatever you want 58 | 4. Select both layers and Register them 59 | 60 | ### Preferences 61 | 62 | ``` 63 | ctrl + alt + cmd + t 64 | ``` 65 | 66 | **Local Registry** 67 | 68 | When registering your typesettings, the `.json` file will save to your `Desktop` by default. You can change the directory (for instance, to a shared folder) from the plugin preferences. 69 | 70 | **Automatic Typesetting** 71 | 72 | - `Enable Automatic Character Spacing`: Automatically sets character spacing—if the typesetting is registered —when deselecting text layers. This is turned off by default. 73 | - `Enable Automatic Line Height`: Automatically sets line height—if the typesetting is registered —when deselecting text layers. This is turned off by default. 74 | 75 | **Ignoring Text Layers** 76 | 77 | When `automatic character spacing` and/or `automatic line height` are enabled, you can ignoring any text layer by adding the prefix `^` or the suffix `^` to text layer name. Note that if you manually typeset the text layers they will not be ignored. 78 | 79 | You can change both the prefix and suffix in the plugin preferences. 80 | 81 | ## (Pre) Registered 82 | 83 | The following typefaces have been registered and automatically included with the plugin. If you'd like to add more, put the typesettings in the `directory` folder and open a pull request. 84 | 85 | - **[SF Pro Display](https://developer.apple.com/fonts)** from iOS resource [guidelines](https://developer.apple.com/design/resources) 86 | - **[SF Pro Text](https://developer.apple.com/fonts)** from iOS resource [guidelines](https://developer.apple.com/design/resources) 87 | - **[SF Compact](https://developer.apple.com/fonts)** from watchOS resource [guidelines](https://developer.apple.com/design/resources) 88 | 89 | ## Development 90 | 91 | _This plugin was created using `skpm`. For a detailed explanation on how things work, checkout the [skpm Readme](https://github.com/skpm/skpm/blob/master/README.md)._ 92 | 93 | -------------------------------------------------------------------------------- /docs/demo.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/buames/typesettings-sketch-plugin/3fd88f8a5c4317fc857c16a17592776619947382/docs/demo.gif -------------------------------------------------------------------------------- /docs/sketch-templates/SanFranciscoTypeface.sketch: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/buames/typesettings-sketch-plugin/3fd88f8a5c4317fc857c16a17592776619947382/docs/sketch-templates/SanFranciscoTypeface.sketch -------------------------------------------------------------------------------- /docs/sketch-templates/TypesettingsStarter.sketch: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/buames/typesettings-sketch-plugin/3fd88f8a5c4317fc857c16a17592776619947382/docs/sketch-templates/TypesettingsStarter.sketch -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "typesettings-sketch-plugin", 3 | "version": "0.2.0", 4 | "description": "A quest for consistency", 5 | "engines": { 6 | "sketch": ">=3.0" 7 | }, 8 | "skpm": { 9 | "name": "Typesettings", 10 | "manifest": "src/plugin/manifest.json", 11 | "main": "typesettings.sketchplugin", 12 | "assets": [ 13 | "src/assets/**/*", 14 | "src/directory/**/*" 15 | ], 16 | "resources": [ 17 | "src/webview/**/*.js" 18 | ] 19 | }, 20 | "scripts": { 21 | "build": "skpm-build", 22 | "watch": "skpm-build --watch", 23 | "start": "skpm-build --watch --run", 24 | "publish": "skpm publish", 25 | "postinstall": "npm run build && skpm-link", 26 | "precommit": "lint-staged", 27 | "lint": "yarn lint:webview && yarn lint:plugin", 28 | "lint:webview": "eslint --ext .js,.jsx src/webview", 29 | "lint:plugin": "eslint --ext .js src/plugin", 30 | "changelog": "lerna-changelog" 31 | }, 32 | "lint-staged": { 33 | "*{.js,.jsx}": [ 34 | "yarn lint --fix", 35 | "git add" 36 | ] 37 | }, 38 | "changelog": { 39 | "labels": { 40 | "Tag: Breaking Change": ":boom: Breaking Change", 41 | "Tag: Enhancement": ":rocket: Enhancement", 42 | "Tag: Bug Fix": ":bug: Bug Fix", 43 | "Tag: Documentation": ":memo: Documentation", 44 | "Tag: Internal": ":house: Internal" 45 | } 46 | }, 47 | "dependencies": { 48 | "@emotion/core": "^0.13.0", 49 | "@emotion/styled": "^0.10.5", 50 | "@skpm/dialog": "^0.2.3", 51 | "@skpm/fs": "^0.1.2", 52 | "@skpm/path": "^0.1.0", 53 | "deepmerge": "^2.1.1", 54 | "prop-types": "^15.6.2", 55 | "react": "^16.4.2", 56 | "react-dom": "^16.4.2", 57 | "react-router": "^4.3.1", 58 | "react-router-config": "^1.0.0-beta.4", 59 | "react-router-dom": "^4.3.1", 60 | "sketch-module-web-view": "^1.2.3", 61 | "svg-inline-react": "^3.1.0", 62 | "typesettings-js": "^0.0.3" 63 | }, 64 | "devDependencies": { 65 | "@babel/core": "^7.0.0", 66 | "@babel/plugin-proposal-class-properties": "^7.0.0", 67 | "@emotion/babel-plugin-core": "^0.6.8", 68 | "@skpm/builder": "^0.5.2", 69 | "@skpm/extract-loader": "^2.0.2", 70 | "babel-eslint": "9.0.0", 71 | "eslint": "5.5.0", 72 | "eslint-config-airbnb": "^17.1.0", 73 | "eslint-config-sketch": "^0.2.4", 74 | "eslint-plugin-import": "^2.14.0", 75 | "eslint-plugin-jsx-a11y": "^6.1.1", 76 | "eslint-plugin-react": "^7.11.1", 77 | "html-loader": "^0.5.5", 78 | "husky": "^0.14.3", 79 | "lerna-changelog": "^0.8.0", 80 | "lint-staged": "^7.2.0", 81 | "skpm": "1.1.4", 82 | "svg-inline-loader": "^0.8.0" 83 | }, 84 | "homepage": "https://github.com/buames/typesettings-sketch-plugin", 85 | "repository": { 86 | "type": "git", 87 | "url": "git+https://github.com/buames/typesettings-sketch-plugin.git" 88 | }, 89 | "author": "buames ", 90 | "license": "MIT" 91 | } 92 | -------------------------------------------------------------------------------- /src/assets/icons/characterSpacingRunner.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/buames/typesettings-sketch-plugin/3fd88f8a5c4317fc857c16a17592776619947382/src/assets/icons/characterSpacingRunner.png -------------------------------------------------------------------------------- /src/assets/icons/lineHeightRunner.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/buames/typesettings-sketch-plugin/3fd88f8a5c4317fc857c16a17592776619947382/src/assets/icons/lineHeightRunner.png -------------------------------------------------------------------------------- /src/assets/icons/mark.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/buames/typesettings-sketch-plugin/3fd88f8a5c4317fc857c16a17592776619947382/src/assets/icons/mark.png -------------------------------------------------------------------------------- /src/assets/icons/registerRunner.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/buames/typesettings-sketch-plugin/3fd88f8a5c4317fc857c16a17592776619947382/src/assets/icons/registerRunner.png -------------------------------------------------------------------------------- /src/directory/SFCompactText.json: -------------------------------------------------------------------------------- 1 | { 2 | "family": "SF Compact Text", 3 | "SFCompactText-Regular": { 4 | "fontFamily": "SF Compact Text", 5 | "fontName": "SFCompactText-Regular", 6 | "fontWeight": 400, 7 | "fontStyle": "normal", 8 | "sources": { 9 | "locals": [ 10 | "SF Compact Text Regular", 11 | "SFCompactText-Regular" 12 | ] 13 | }, 14 | "normalcase": { 15 | "11": { 16 | "characterSpacing": 0.3, 17 | "lineHeight": 13.5, 18 | "paragraphSpacing": 0 19 | }, 20 | "12": { 21 | "characterSpacing": 0.2, 22 | "lineHeight": 14.5, 23 | "paragraphSpacing": 0 24 | }, 25 | "13": { 26 | "characterSpacing": 0.15, 27 | "lineHeight": 15.5, 28 | "paragraphSpacing": 0 29 | }, 30 | "14": { 31 | "characterSpacing": 0.25, 32 | "lineHeight": 16.5, 33 | "paragraphSpacing": 0 34 | }, 35 | "15": { 36 | "characterSpacing": 0.1, 37 | "lineHeight": 17.5, 38 | "paragraphSpacing": 0 39 | }, 40 | "16": { 41 | "characterSpacing": 0, 42 | "lineHeight": 18.5, 43 | "paragraphSpacing": 0 44 | }, 45 | "17": { 46 | "characterSpacing": 0, 47 | "lineHeight": 19.5, 48 | "paragraphSpacing": 0 49 | }, 50 | "18": { 51 | "characterSpacing": -0.05, 52 | "lineHeight": 20.5, 53 | "paragraphSpacing": 0 54 | }, 55 | "19": { 56 | "characterSpacing": 0, 57 | "lineHeight": 21.5, 58 | "paragraphSpacing": 0 59 | }, 60 | "20": { 61 | "characterSpacing": 0, 62 | "lineHeight": 22.5, 63 | "paragraphSpacing": 0 64 | }, 65 | "21": { 66 | "characterSpacing": 0, 67 | "lineHeight": 23.5, 68 | "paragraphSpacing": 0 69 | }, 70 | "22": { 71 | "characterSpacing": 0, 72 | "lineHeight": 24.5, 73 | "paragraphSpacing": 0 74 | }, 75 | "24": { 76 | "characterSpacing": 0, 77 | "lineHeight": 26.5, 78 | "paragraphSpacing": 0 79 | }, 80 | "26": { 81 | "characterSpacing": -0.1000000014901161, 82 | "lineHeight": 26.5, 83 | "paragraphSpacing": 0 84 | }, 85 | "27": { 86 | "characterSpacing": -0.1000000014901161, 87 | "lineHeight": 26.5, 88 | "paragraphSpacing": 0 89 | }, 90 | "28": { 91 | "characterSpacing": -0.2000000029802322, 92 | "lineHeight": 26.5, 93 | "paragraphSpacing": 0 94 | }, 95 | "30": { 96 | "characterSpacing": -0.1000000014901161, 97 | "lineHeight": 26.5, 98 | "paragraphSpacing": 0 99 | }, 100 | "31": { 101 | "characterSpacing": -0.1000000014901161, 102 | "lineHeight": 26.5, 103 | "paragraphSpacing": 0 104 | }, 105 | "32": { 106 | "characterSpacing": -0.2000000029802322, 107 | "lineHeight": 34.5, 108 | "paragraphSpacing": 0 109 | }, 110 | "34": { 111 | "characterSpacing": -0.1000000014901161, 112 | "lineHeight": 26.5, 113 | "paragraphSpacing": 0 114 | }, 115 | "36": { 116 | "characterSpacing": -0.699999988079071, 117 | "lineHeight": 38.5, 118 | "paragraphSpacing": 0 119 | }, 120 | "38": { 121 | "characterSpacing": -0.699999988079071, 122 | "lineHeight": 40.5, 123 | "paragraphSpacing": 0 124 | }, 125 | "39": { 126 | "characterSpacing": -0.699999988079071, 127 | "lineHeight": 41.5, 128 | "paragraphSpacing": 0 129 | }, 130 | "40": { 131 | "characterSpacing": -0.699999988079071, 132 | "lineHeight": 42.5, 133 | "paragraphSpacing": 0 134 | }, 135 | "41": { 136 | "characterSpacing": -0.699999988079071, 137 | "lineHeight": 43.5, 138 | "paragraphSpacing": 0 139 | }, 140 | "42": { 141 | "characterSpacing": -0.699999988079071, 142 | "lineHeight": 44.5, 143 | "paragraphSpacing": 0 144 | } 145 | } 146 | }, 147 | "SFCompactText-Semibold": { 148 | "fontFamily": "SF Compact Text", 149 | "fontName": "SFCompactText-Semibold", 150 | "fontWeight": 600, 151 | "fontStyle": "normal", 152 | "sources": { 153 | "locals": [ 154 | "SF Compact Text Semibold", 155 | "SFCompactText-Semibold" 156 | ] 157 | }, 158 | "normalcase": { 159 | "14": { 160 | "characterSpacing": 0.15, 161 | "lineHeight": 16.5, 162 | "paragraphSpacing": 0 163 | }, 164 | "15": { 165 | "characterSpacing": 0.05, 166 | "lineHeight": 17.5, 167 | "paragraphSpacing": 0 168 | }, 169 | "16": { 170 | "characterSpacing": 0.05, 171 | "lineHeight": 18.5, 172 | "paragraphSpacing": 0 173 | }, 174 | "17": { 175 | "characterSpacing": 0, 176 | "lineHeight": 19.5, 177 | "paragraphSpacing": 0 178 | }, 179 | "18": { 180 | "characterSpacing": -0.1, 181 | "lineHeight": 20.5, 182 | "paragraphSpacing": 0 183 | }, 184 | "19": { 185 | "characterSpacing": -0.15, 186 | "lineHeight": 21.5, 187 | "paragraphSpacing": 0 188 | } 189 | } 190 | }, 191 | "lastUpdated": "2018-09-03T17:25:31.947Z", 192 | "version": "0.1.1", 193 | "compatibleVersion": "0.0.3" 194 | } -------------------------------------------------------------------------------- /src/directory/SFProDisplay.json: -------------------------------------------------------------------------------- 1 | { 2 | "family": "SF Pro Display", 3 | "SFProDisplay-Regular": { 4 | "fontFamily": "SF Pro Display", 5 | "fontName": "SFProDisplay-Regular", 6 | "fontWeight": 400, 7 | "fontStyle": "normal", 8 | "sources": { 9 | "locals": [ 10 | "SF Pro Display Regular", 11 | "SFProDisplay-Regular" 12 | ] 13 | }, 14 | "normalcase": { 15 | "20": { 16 | "characterSpacing": 0.3799999952316284, 17 | "lineHeight": 25, 18 | "paragraphSpacing": 0 19 | }, 20 | "21": { 21 | "characterSpacing": 0.3600000143051147, 22 | "lineHeight": 26, 23 | "paragraphSpacing": 0 24 | }, 25 | "22": { 26 | "characterSpacing": 0.3499999940395355, 27 | "lineHeight": 28, 28 | "paragraphSpacing": 0 29 | }, 30 | "23": { 31 | "characterSpacing": 0.3700000047683716, 32 | "lineHeight": 29, 33 | "paragraphSpacing": 0 34 | }, 35 | "24": { 36 | "characterSpacing": 0.3600000143051147, 37 | "lineHeight": 30, 38 | "paragraphSpacing": 0 39 | }, 40 | "25": { 41 | "characterSpacing": 0.3499999940395355, 42 | "lineHeight": 31, 43 | "paragraphSpacing": 0 44 | }, 45 | "26": { 46 | "characterSpacing": 0.3600000143051147, 47 | "lineHeight": 32, 48 | "paragraphSpacing": 0 49 | }, 50 | "27": { 51 | "characterSpacing": 0.3499999940395355, 52 | "lineHeight": 33, 53 | "paragraphSpacing": 0 54 | }, 55 | "28": { 56 | "characterSpacing": 0.3600000143051147, 57 | "lineHeight": 34, 58 | "paragraphSpacing": 0 59 | }, 60 | "29": { 61 | "characterSpacing": 0.3199999928474426, 62 | "lineHeight": 35, 63 | "paragraphSpacing": 0 64 | }, 65 | "30": { 66 | "characterSpacing": 0.36, 67 | "lineHeight": 37, 68 | "paragraphSpacing": 0 69 | }, 70 | "31": { 71 | "characterSpacing": 0.4000000059604645, 72 | "lineHeight": 38, 73 | "paragraphSpacing": 0 74 | }, 75 | "32": { 76 | "characterSpacing": 0.3799999952316284, 77 | "lineHeight": 39, 78 | "paragraphSpacing": 0 79 | }, 80 | "33": { 81 | "characterSpacing": 0.3799999952316284, 82 | "lineHeight": 40, 83 | "paragraphSpacing": 0 84 | }, 85 | "34": { 86 | "characterSpacing": 0.3700000047683716, 87 | "lineHeight": 41, 88 | "paragraphSpacing": 0 89 | }, 90 | "36": { 91 | "characterSpacing": 0.3799999952316284, 92 | "lineHeight": 43, 93 | "paragraphSpacing": 0 94 | }, 95 | "37": { 96 | "characterSpacing": 0.4799999892711639, 97 | "lineHeight": 44, 98 | "paragraphSpacing": 0 99 | }, 100 | "38": { 101 | "characterSpacing": 0.4199999868869781, 102 | "lineHeight": 46, 103 | "paragraphSpacing": 0 104 | }, 105 | "39": { 106 | "characterSpacing": 0.5400000214576721, 107 | "lineHeight": 47, 108 | "paragraphSpacing": 0 109 | }, 110 | "40": { 111 | "characterSpacing": 0.4000000059604645, 112 | "lineHeight": 48, 113 | "paragraphSpacing": 0 114 | }, 115 | "42": { 116 | "characterSpacing": 0.5199999809265137, 117 | "lineHeight": 50, 118 | "paragraphSpacing": 0 119 | }, 120 | "43": { 121 | "characterSpacing": 0.449999988079071, 122 | "lineHeight": 51, 123 | "paragraphSpacing": 0 124 | }, 125 | "44": { 126 | "characterSpacing": 0.3100000023841858, 127 | "lineHeight": 52, 128 | "paragraphSpacing": 0 129 | }, 130 | "47": { 131 | "characterSpacing": 1.059999942779541, 132 | "lineHeight": 56, 133 | "paragraphSpacing": 0 134 | }, 135 | "48": { 136 | "characterSpacing": 0.2899999916553497, 137 | "lineHeight": 57, 138 | "paragraphSpacing": 0 139 | }, 140 | "49": { 141 | "characterSpacing": 0.3600000143051147, 142 | "lineHeight": 58, 143 | "paragraphSpacing": 0 144 | }, 145 | "50": { 146 | "characterSpacing": 0.3199999928474426, 147 | "lineHeight": 59, 148 | "paragraphSpacing": 0 149 | }, 150 | "51": { 151 | "characterSpacing": 0.3499999940395355, 152 | "lineHeight": 60, 153 | "paragraphSpacing": 0 154 | }, 155 | "52": { 156 | "characterSpacing": 0.3799999952316284, 157 | "lineHeight": 61, 158 | "paragraphSpacing": 0 159 | }, 160 | "53": { 161 | "characterSpacing": 0.3700000047683716, 162 | "lineHeight": 62, 163 | "paragraphSpacing": 0 164 | }, 165 | "55": { 166 | "characterSpacing": 0.33, 167 | "lineHeight": 65, 168 | "paragraphSpacing": 0 169 | }, 170 | "56": { 171 | "characterSpacing": 0.3100000023841858, 172 | "lineHeight": 66, 173 | "paragraphSpacing": 0 174 | }, 175 | "58": { 176 | "characterSpacing": 0.300000011920929, 177 | "lineHeight": 68, 178 | "paragraphSpacing": 0 179 | }, 180 | "60": { 181 | "characterSpacing": 0.25, 182 | "lineHeight": 70, 183 | "paragraphSpacing": 0 184 | } 185 | } 186 | }, 187 | "SFProDisplay-Semibold": { 188 | "fontFamily": "SF Pro Display", 189 | "fontName": "SFProDisplay-Semibold", 190 | "fontWeight": 600, 191 | "fontStyle": "normal", 192 | "sources": { 193 | "locals": [ 194 | "SF Pro Display Semibold", 195 | "SFProDisplay-Semibold" 196 | ] 197 | }, 198 | "normalcase": { 199 | "21": { 200 | "characterSpacing": 0.3600000143051147, 201 | "lineHeight": 26, 202 | "paragraphSpacing": 0 203 | }, 204 | "23": { 205 | "characterSpacing": 0.3700000047683716, 206 | "lineHeight": 29, 207 | "paragraphSpacing": 0 208 | }, 209 | "28": { 210 | "characterSpacing": 0.3400000035762787, 211 | "lineHeight": 34, 212 | "paragraphSpacing": 0 213 | }, 214 | "33": { 215 | "characterSpacing": 0.3899999856948853, 216 | "lineHeight": 40, 217 | "paragraphSpacing": 0 218 | }, 219 | "40": { 220 | "characterSpacing": 0.4099999964237213, 221 | "lineHeight": 48, 222 | "paragraphSpacing": 0 223 | }, 224 | "47": { 225 | "characterSpacing": 0.3600000143051147, 226 | "lineHeight": 56, 227 | "paragraphSpacing": 0 228 | }, 229 | "53": { 230 | "characterSpacing": 0.25, 231 | "lineHeight": 62, 232 | "paragraphSpacing": 0 233 | } 234 | } 235 | }, 236 | "lastUpdated": "2018-09-03T17:25:41.817Z", 237 | "version": "0.1.1", 238 | "compatibleVersion": "0.0.3" 239 | } -------------------------------------------------------------------------------- /src/directory/SFProText.json: -------------------------------------------------------------------------------- 1 | { 2 | "family": "SF Pro Text", 3 | "SFProText-Regular": { 4 | "fontFamily": "SF Pro Text", 5 | "fontName": "SFProText-Regular", 6 | "fontWeight": 400, 7 | "fontStyle": "normal", 8 | "sources": { 9 | "locals": [ 10 | "SF Pro Text Regular", 11 | "SFProText-Regular" 12 | ] 13 | }, 14 | "normalcase": { 15 | "11": { 16 | "characterSpacing": 0.07000000029802322, 17 | "lineHeight": 13, 18 | "paragraphSpacing": 0 19 | }, 20 | "12": { 21 | "characterSpacing": 0, 22 | "lineHeight": 16, 23 | "paragraphSpacing": 0 24 | }, 25 | "13": { 26 | "characterSpacing": -0.07999999821186066, 27 | "lineHeight": 18, 28 | "paragraphSpacing": 0 29 | }, 30 | "14": { 31 | "characterSpacing": -0.1500000059604645, 32 | "lineHeight": 19, 33 | "paragraphSpacing": 0 34 | }, 35 | "15": { 36 | "characterSpacing": -0.239999994635582, 37 | "lineHeight": 20, 38 | "paragraphSpacing": 0 39 | }, 40 | "16": { 41 | "characterSpacing": -0.3199999928474426, 42 | "lineHeight": 21, 43 | "paragraphSpacing": 0 44 | }, 45 | "17": { 46 | "characterSpacing": -0.4099999964237213, 47 | "lineHeight": 22, 48 | "paragraphSpacing": 0 49 | }, 50 | "18": { 51 | "characterSpacing": -0.449999988079071, 52 | "lineHeight": 23, 53 | "paragraphSpacing": 0 54 | }, 55 | "19": { 56 | "characterSpacing": -0.4900000095367432, 57 | "lineHeight": 24, 58 | "paragraphSpacing": 0 59 | } 60 | } 61 | }, 62 | "SFProText-Semibold": { 63 | "fontFamily": "SF Pro Text", 64 | "fontName": "SFProText-Semibold", 65 | "fontWeight": 600, 66 | "fontStyle": "normal", 67 | "sources": { 68 | "locals": [ 69 | "SF Pro Text Semibold", 70 | "SFProText-Semibold" 71 | ] 72 | }, 73 | "normalcase": { 74 | "14": { 75 | "characterSpacing": -0.1500000059604645, 76 | "lineHeight": 19, 77 | "paragraphSpacing": 0 78 | }, 79 | "15": { 80 | "characterSpacing": -0.239999994635582, 81 | "lineHeight": 20, 82 | "paragraphSpacing": 0 83 | }, 84 | "16": { 85 | "characterSpacing": -0.3199999928474426, 86 | "lineHeight": 21, 87 | "paragraphSpacing": 0 88 | }, 89 | "17": { 90 | "characterSpacing": -0.4099999964237213, 91 | "lineHeight": 22, 92 | "paragraphSpacing": 0 93 | }, 94 | "19": { 95 | "characterSpacing": -0.4900000095367432, 96 | "lineHeight": 24, 97 | "paragraphSpacing": 0 98 | } 99 | } 100 | }, 101 | "lastUpdated": "2018-09-03T17:25:41.817Z", 102 | "version": "0.1.1", 103 | "compatibleVersion": "0.0.3" 104 | } -------------------------------------------------------------------------------- /src/plugin/Typesetter.js: -------------------------------------------------------------------------------- 1 | import fs from '@skpm/fs' 2 | import { 3 | getLetterCasing, 4 | getStyleOfFont, 5 | getWeightOfFont 6 | } from 'plugin/utils/fonts' 7 | import { MIN_VERSION, preferences } from './storage' 8 | 9 | const filePath = (fontFamily) => { 10 | const fileName = `${ fontFamily.replace(/\s/g, '') }.json` 11 | 12 | const pluginDefinedDirectory = `${ preferences.pluginDefinedDirectory }/${ fileName }` 13 | if (fs.existsSync(pluginDefinedDirectory)) { 14 | return pluginDefinedDirectory 15 | } 16 | 17 | // If the user defined directory path already includes the home path, 18 | // we don't want to prepend it or its not going to work correctly 19 | const userDirPath = `${ preferences.userDefinedDirectory }/${ fileName }` 20 | 21 | if (userDirPath.includes(String(NSHomeDirectory()))) { 22 | return userDirPath 23 | } 24 | 25 | return `${ NSHomeDirectory() }/${ userDirPath }` 26 | } 27 | 28 | const transform = (textLayer) => { 29 | const layer = textLayer.sketchObject || textLayer 30 | const attrs = layer.style().textStyle().attributes() 31 | 32 | return { 33 | fontFamily: String(attrs.NSFont.familyName()), 34 | fontName: String(attrs.NSFont.fontName()), 35 | fontDisplayName: String(attrs.NSFont.displayName()), 36 | fontPostscriptName: String(layer.fontPostscriptName()), 37 | fontSize: Number(layer.fontSize()), 38 | fontStyle: String(getStyleOfFont(attrs.NSFont)), 39 | fontWeight: Number(getWeightOfFont(attrs.NSFont)), 40 | casing: getLetterCasing(attrs), 41 | characterSpacing: layer.characterSpacing() 42 | ? Number(layer.characterSpacing()) 43 | : layer.characterSpacing(), 44 | lineHeight: Number(layer.lineHeight()), 45 | paragraphSpacing: Number(attrs.NSParagraphStyle.paragraphSpacing()) 46 | } 47 | } 48 | 49 | const toVariant = textLayer => ({ 50 | [textLayer.fontName]: { 51 | fontFamily: textLayer.fontFamily, 52 | fontName: textLayer.fontName, 53 | fontWeight: textLayer.fontWeight, 54 | fontStyle: textLayer.fontStyle, 55 | sources: { 56 | locals: [ 57 | textLayer.fontDisplayName, 58 | textLayer.fontPostscriptName 59 | ] 60 | }, 61 | [textLayer.casing]: { 62 | [textLayer.fontSize]: { 63 | characterSpacing: textLayer.characterSpacing, 64 | lineHeight: textLayer.lineHeight, 65 | paragraphSpacing: textLayer.paragraphSpacing 66 | } 67 | } 68 | } 69 | }) 70 | 71 | // Returns typesettings for a given text layer 72 | let typesettings = null 73 | const fetch = (context, layer) => { 74 | const { 75 | fontFamily, 76 | fontSize, 77 | fontName, 78 | casing 79 | } = transform(layer) 80 | 81 | if (!typesettings || (typesettings.family !== fontFamily)) { 82 | const file = filePath(fontFamily) 83 | 84 | if (!fs.existsSync(file)) { 85 | return 'There are no registered typesettings for the text layer.' 86 | } 87 | 88 | try { 89 | typesettings = JSON.parse(fs.readFileSync(file, 'utf8')) 90 | } catch (err) { 91 | return err 92 | } 93 | } 94 | 95 | const version = context.plugin.version().UTF8String() 96 | const { compatibleVersion } = typesettings 97 | 98 | // Check if the settings are compatible 99 | // (v0.0.2 and below will not have a compatibleVersion) 100 | if (!compatibleVersion || compatibleVersion < Number(MIN_VERSION)) { 101 | if (!typesettings[fontName] 102 | || !typesettings[fontName][fontSize] 103 | || !typesettings[fontName][fontSize][casing]) { 104 | return 'No typesettings registered for the current casing and font size.' 105 | } 106 | return typesettings[fontName][fontSize][casing] 107 | } 108 | 109 | // Check if plugin is out of date and incompatible with a newer typesettings version 110 | if (compatibleVersion && compatibleVersion > version) { 111 | return 'Your plugin is out of date. Please update to the latest version of Typesettings.' 112 | } 113 | 114 | // Latest Version 115 | if (compatibleVersion && compatibleVersion <= version) { 116 | if (!typesettings[fontName] 117 | || !typesettings[fontName][casing] 118 | || !typesettings[fontName][casing][fontSize]) { 119 | return 'No typesettings registered for the current casing and font size.' 120 | } 121 | return typesettings[fontName][casing][fontSize] 122 | } 123 | 124 | return 'There are no registered typesettings for the text layer.' 125 | } 126 | 127 | const setType = (layer, settings, opts) => { 128 | const { kern, lineHeight } = opts 129 | 130 | if (kern) { 131 | layer.setCharacterSpacing(settings.characterSpacing) 132 | } 133 | 134 | if (lineHeight) { 135 | layer.style().textStyle().attributes().NSParagraphStyle 136 | .setParagraphSpacing(settings.paragraphSpacing) 137 | layer.setLineHeight(settings.lineHeight) 138 | } 139 | } 140 | 141 | const Typesetter = { 142 | filePath, 143 | transform, 144 | toVariant, 145 | fetch, 146 | setType 147 | } 148 | 149 | export default Typesetter 150 | -------------------------------------------------------------------------------- /src/plugin/commands/auto.js: -------------------------------------------------------------------------------- 1 | import Typesetter from 'plugin/Typesetter' 2 | import { preferences } from 'plugin/storage' 3 | import { getMSTextLayers } from 'plugin/utils/helpers' 4 | 5 | export const onSelectionFinish = (context) => { 6 | const { 7 | allowsAutoKerning, 8 | allowsAutoLineHeight, 9 | ignorePrefix, 10 | ignoreSuffix 11 | } = preferences 12 | 13 | if (allowsAutoKerning || allowsAutoLineHeight) { 14 | const selection = getMSTextLayers(context.actionContext.oldSelection) 15 | 16 | selection.forEach((layer) => { 17 | // If the layer name matches the ignore prefix or suffix, bail 18 | const name = layer.name() 19 | if (name.hasPrefix(ignorePrefix) || name.hasSuffix(ignoreSuffix)) { 20 | return 21 | } 22 | 23 | // Get the typesettings for the given layer 24 | const settings = Typesetter.fetch(context, layer) 25 | 26 | if (typeof settings === 'string') { 27 | return 28 | } 29 | 30 | // If we don't have settings, bail 31 | if (settings && settings.length === 0) { 32 | return 33 | } 34 | 35 | Typesetter.setType(layer, settings, { 36 | kern: allowsAutoKerning, 37 | lineHeight: allowsAutoLineHeight 38 | }) 39 | }) 40 | } 41 | } 42 | 43 | export default onSelectionFinish 44 | -------------------------------------------------------------------------------- /src/plugin/commands/preferences.js: -------------------------------------------------------------------------------- 1 | import dialog from '@skpm/dialog' 2 | import UI from 'sketch/ui' 3 | import BrowserWindow from 'sketch-module-web-view' 4 | import WebviewEntry from 'webview/index.html' 5 | import { preferences, savePreferences } from 'plugin/storage' 6 | 7 | export default () => { 8 | const options = { 9 | identifier: 'typesettings', 10 | redirectTo: '/', 11 | width: 310, 12 | height: 500, 13 | minWidth: 260, 14 | minHeight: 450, 15 | show: false, 16 | loaded: false, 17 | backgroundColor: '#FFFFFF', 18 | fullscreenable: false, 19 | devTools: true 20 | } 21 | 22 | const window = new BrowserWindow(options) 23 | const { webContents } = window 24 | 25 | // Only show the window when the page has loaded 26 | window.once('ready-to-show', () => { 27 | window.show() 28 | }) 29 | 30 | // Returns the plugin preferences to the webview 31 | webContents.on('getPreferences', () => { 32 | webContents.executeJavaScript(`preferences = ${ JSON.stringify(preferences) }`) 33 | }) 34 | 35 | // Updates the plugin preferences from the webview 36 | webContents.on('setPreferences', (newState) => { 37 | const newPrefs = savePreferences(newState) 38 | webContents.executeJavaScript(`reloadData(${ JSON.stringify(newPrefs) })`) 39 | }) 40 | 41 | // Prompts to select a local directory file path 42 | webContents.on('selectUserDefinedDirectory', (newState) => { 43 | const opts = { 44 | buttonLabel: 'Set Directory', 45 | properties: [ 'openDirectory' ], 46 | filters: [ 47 | { name: 'DirectoriesOnly', extensions: [ '' ] } 48 | ] 49 | } 50 | 51 | dialog.showOpenDialog(opts, ((selection) => { 52 | const newPrefs = savePreferences({ 53 | ...newState, 54 | userDefinedDirectory: selection[0] 55 | }) 56 | webContents.executeJavaScript(`reloadData(${ JSON.stringify(newPrefs) })`) 57 | })) 58 | }) 59 | 60 | // Copies the userDefinedDirectory to the clipboard 61 | webContents.on('copyUserDefinedDirectoryPath', () => { 62 | const pasteboard = NSPasteboard.generalPasteboard() 63 | pasteboard.clearContents() 64 | pasteboard.setString_forType_(preferences.userDefinedDirectory, NSStringPboardType) // eslint-disable-line 65 | UI.message('Copied to clipboard!') 66 | }) 67 | 68 | // Copies the userDefinedDirectory to the clipboard 69 | webContents.on('showUserDefinedDirectory', () => ( 70 | NSWorkspace.sharedWorkspace() 71 | .selectFile_inFileViewerRootedAtPath(preferences.userDefinedDirectory, null) 72 | )) 73 | 74 | // Open an external url 75 | webContents.on('openUrl', url => ( 76 | NSWorkspace.sharedWorkspace() 77 | .openURL(NSURL.URLWithString(url)) 78 | )) 79 | 80 | window.loadURL(WebviewEntry) 81 | } 82 | -------------------------------------------------------------------------------- /src/plugin/commands/register.js: -------------------------------------------------------------------------------- 1 | import UI from 'sketch/ui' 2 | import fs from '@skpm/fs' 3 | import dialog from '@skpm/dialog' 4 | import merge from 'deepmerge' 5 | import Typesetter from 'plugin/Typesetter' 6 | import { MIN_VERSION } from 'plugin/storage' 7 | import { getJSTextLayers, pluck } from 'plugin/utils/helpers' 8 | 9 | const mergeOptions = { 10 | arrayMerge: (destinationArray, sourceArray, options) => sourceArray // eslint-disable-line 11 | } 12 | 13 | export default (context) => { 14 | const selection = getJSTextLayers(context.selection) 15 | 16 | if (selection.length === 0) { 17 | return UI.message('You need to select atleast 1 text layer') 18 | } 19 | 20 | const version = context.plugin.version().UTF8String() 21 | const lastUpdated = new Date().toISOString() 22 | const textLayers = selection.map(Typesetter.transform) 23 | 24 | // Get all of the selected font families 25 | const families = pluck(textLayers, 'fontFamily') 26 | 27 | // Create the typesettings for each font family 28 | const typesettings = families.map((family) => { 29 | const fonts = textLayers.filter(layer => layer.fontFamily === family) 30 | const variants = merge.all(fonts.map(Typesetter.toVariant), mergeOptions) 31 | return { 32 | family, 33 | ...variants, 34 | lastUpdated, 35 | version, // plugin version used to register typesettings 36 | compatibleVersion: MIN_VERSION // min plugin version to load typesettings 37 | } 38 | }) 39 | 40 | // Replace or update the typesettings 41 | const done = typesettings.map((settings) => { 42 | const filePath = Typesetter.filePath(settings.family) 43 | 44 | if (fs.existsSync(filePath)) { 45 | const currSettings = JSON.parse(fs.readFileSync(filePath, 'utf8')) 46 | const { compatibleVersion } = currSettings 47 | let status = '' 48 | 49 | // Check if plugin is out of date and incompatible with a newer typesettings version 50 | if (compatibleVersion && compatibleVersion > version) { 51 | status = 'Your plugin is out of date. Please update to the latest version of Typesettings.' 52 | } 53 | 54 | // Check if the existing settings are compatible and override if not 55 | // (v0.0.2 and below will not have a compatibleVersion) 56 | if (!compatibleVersion || compatibleVersion < Number(MIN_VERSION)) { 57 | dialog.showMessageBox({ 58 | message: `Incompatible Typesetting Versions for ${ settings.family }`, 59 | detail: `Your typesettings were created with an incompatible version of the Typesettings plugin. Would you like to replace all typesettings for ${ settings.family }?`, 60 | buttons: [ 'Replace', 'Cancel' ] 61 | }, ({ response }) => { 62 | if (response === 1) { 63 | status = 'Nothing saved' 64 | } 65 | 66 | if (response === 0) { 67 | fs.writeFileSync(filePath, JSON.stringify(settings, null, 2)) 68 | status = `Replaced ${ settings.family } typesettings` 69 | } 70 | }) 71 | } else { 72 | // Latest Version 73 | dialog.showMessageBox({ 74 | message: `Typesettings Exists for ${ settings.family }`, 75 | detail: `Do you want to merge or replace current typesettings for ${ settings.family }?`, 76 | buttons: [ 'Merge', 'Replace', 'Cancel' ] 77 | }, ({ response }) => { 78 | // Cancel clicked 79 | if (response === 2) { 80 | status = 'Nothing saved' 81 | } 82 | 83 | // Replace clicked 84 | if (response === 1) { 85 | fs.writeFileSync(filePath, JSON.stringify(settings, null, 2)) 86 | status = `Replaced ${ settings.family } typesettings` 87 | } 88 | 89 | if (response === 0) { 90 | // Merge clicked 91 | const updatedSettings = merge(currSettings, settings, mergeOptions) 92 | fs.writeFileSync(filePath, JSON.stringify(updatedSettings, null, 2)) 93 | status = `Updated ${ settings.family } typesettings` 94 | } 95 | }) 96 | } 97 | 98 | return status 99 | } 100 | 101 | // Typesettings for not exists. Let's write them all out. 102 | fs.writeFileSync(filePath, JSON.stringify(settings, null, 2)) 103 | return `Registered ${ settings.family } typesettings` 104 | }) 105 | 106 | const msg = (done.length === 1) ? done.join('') : `Registered typesettings for ${ done.length } fonts` 107 | return UI.message(msg) 108 | } 109 | -------------------------------------------------------------------------------- /src/plugin/commands/typeset.js: -------------------------------------------------------------------------------- 1 | import UI from 'sketch/ui' 2 | import Typesetter from 'plugin/Typesetter' 3 | import { getMSTextLayers } from 'plugin/utils/helpers' 4 | 5 | const typeset = (context, opts) => { 6 | const { setCharacterSpacing, setLineHeight } = opts 7 | const selection = getMSTextLayers(context.selection) 8 | 9 | if (selection.length === 0) { 10 | return 'You need to select atleast 1 text layer' 11 | } 12 | 13 | const counter = { set: 0, skipped: 0 } 14 | 15 | selection.forEach((layer) => { 16 | const settings = Typesetter.fetch(context, layer) 17 | 18 | if (typeof settings === 'string') { 19 | counter.skipped += 1 20 | UI.message(settings) 21 | return 22 | } 23 | 24 | if (settings && settings.length === 0) { 25 | counter.skipped += 1 26 | return 27 | } 28 | 29 | Typesetter.setType(layer, settings, { 30 | kern: setCharacterSpacing, 31 | lineHeight: setLineHeight 32 | }) 33 | counter.set += 1 34 | }) 35 | 36 | // Reload and return the count 37 | context.document.reloadInspector() 38 | 39 | if (counter.set === 0 && counter.skipped > 0) { 40 | return 'There are no registered typesettings for the text layer.' 41 | } 42 | 43 | return `Set: ${ counter.set }, Skipped: ${ counter.skipped }` 44 | } 45 | 46 | // onRun Typset Character Spacing 47 | export const onSetCharacterSpacing = (context) => { 48 | const done = typeset(context, { 49 | setCharacterSpacing: true, 50 | setLineHeight: false 51 | }) 52 | UI.message(done) 53 | } 54 | 55 | // onRun Typset Line Height 56 | export const onSetLineHeight = (context) => { 57 | const done = typeset(context, { 58 | setCharacterSpacing: false, 59 | setLineHeight: true 60 | }) 61 | UI.message(done) 62 | } 63 | -------------------------------------------------------------------------------- /src/plugin/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "compatibleVersion": 3, 3 | "bundleVersion": 1, 4 | "icon": "assets/icons/mark.png", 5 | "commands": [ 6 | { 7 | "name": "Automatically Typeset Text Layers", 8 | "handlers": { 9 | "actions": { 10 | "SelectionChanged.finish": "onSelectionFinish" 11 | } 12 | }, 13 | "script": "commands/auto.js" 14 | }, 15 | { 16 | "name": "Set Character Spacing", 17 | "description": "Set selected text layer's character spacing if the typesettings are registered.", 18 | "shortcut" : "cmd shift →", 19 | "icon": "assets/icons/characterSpacingRunner.png", 20 | "handlers": { 21 | "run": "onSetCharacterSpacing" 22 | }, 23 | "identifier": "typesetCharacterSpacing", 24 | "script": "commands/typeset.js" 25 | }, 26 | { 27 | "name": "Set Line Height", 28 | "description": "Set selected text layer's line height if the typesettings are registered.", 29 | "shortcut" : "cmd shift ↓", 30 | "icon": "assets/icons/lineHeightRunner.png", 31 | "handlers": { 32 | "run": "onSetLineHeight" 33 | }, 34 | "identifier": "typesetLineHeight", 35 | "script": "commands/typeset.js" 36 | }, 37 | { 38 | "name": "Register Typesettings", 39 | "description": "Register typsettings of all selected layers.", 40 | "shortcut" : "ctrl alt cmd ↩", 41 | "icon": "assets/icons/registerRunner.png", 42 | "identifier": "registerTypesettings", 43 | "script": "commands/register.js" 44 | }, 45 | { 46 | "name": "Preferences", 47 | "description": "Manage your Typesettings Plugin preferences.", 48 | "shortcut" : "ctrl alt cmd t", 49 | "icon": "assets/icons/mark.png", 50 | "identifier": "preferences", 51 | "script": "commands/preferences.js" 52 | } 53 | ], 54 | "menu": { 55 | "title": "Typesettings", 56 | "items": [ 57 | "typesetCharacterSpacing", 58 | "typesetLineHeight", 59 | "-", 60 | "registerTypesettings", 61 | "preferences" 62 | ] 63 | } 64 | } 65 | -------------------------------------------------------------------------------- /src/plugin/storage/index.js: -------------------------------------------------------------------------------- 1 | import Settings from 'sketch/settings' 2 | import { __DEV__ } from 'plugin/utils/helpers' 3 | 4 | export const MIN_VERSION = '0.0.3' 5 | 6 | export const preferences = { 7 | pluginDefinedDirectory: __DEV__ 8 | ? `${ NSHomeDirectory() }/Development/typesettings-sketch-plugin/src/directory` 9 | : `${ NSHomeDirectory() }/Library/Application Support/com.bohemiancoding.sketch3/Plugins/typesettings.sketchplugin/Contents/Resources`, 10 | userDefinedDirectory: Settings.settingForKey('userDefinedDirectory') || `${ NSHomeDirectory() }/Desktop`, 11 | allowsAutoKerning: Settings.settingForKey('allowsAutoKerning') || false, 12 | allowsAutoLineHeight: Settings.settingForKey('allowsAutoLineHeight') || false, 13 | ignorePrefix: Settings.settingForKey('ignorePrefix') || '^', 14 | ignoreSuffix: Settings.settingForKey('ignoreSuffix') || '^' 15 | } 16 | 17 | export const savePreferences = (newPrefs) => { 18 | const prefs = { ...preferences, ...newPrefs } 19 | Object.keys(prefs).forEach((key) => { 20 | Settings.setSettingForKey(key, prefs[key]) 21 | }) 22 | return prefs 23 | } 24 | -------------------------------------------------------------------------------- /src/plugin/utils/fonts.js: -------------------------------------------------------------------------------- 1 | export const TEXT_TRANSFORM = { 2 | 0: 'normalcase', 3 | 1: 'uppercase', 4 | 2: 'lowercase' 5 | } 6 | 7 | export const TEXT_ALIGNMENT = { 8 | 0: 'left', 9 | 1: 'right', 10 | 2: 'center', 11 | 3: 'justified' 12 | } 13 | 14 | export const FONT_STYLES = { 15 | 0: 'normal', 16 | 1: 'italic' 17 | } 18 | 19 | // Maps AppKit weightOfFont_ to CSS Values 20 | // See https://github.com/chromium/chromium/blob/1fa6069561057a05c66dc1a7e5b3c5a4beb519c6/third_party/blink/renderer/platform/fonts/mac/font_family_matcher_mac.mm#L293-L313 21 | export const FONT_WEIGHTS = { 22 | 2: 100, 23 | 3: 200, 24 | 4: 300, 25 | 5: 400, 26 | 6: 500, 27 | 8: 600, 28 | 9: 700, 29 | 10: 800, 30 | 12: 900 31 | } 32 | 33 | export const isItalicFont = (font) => { 34 | const traits = font.fontDescriptor().objectForKey(NSFontTraitsAttribute) 35 | const symbolicTraits = traits[NSFontSymbolicTrait].unsignedIntValue() 36 | return (symbolicTraits & NSFontItalicTrait) !== 0 //eslint-disable-line 37 | } 38 | 39 | export const getStyleOfFont = (font) => { 40 | const isItalic = isItalicFont(font) ? 1 : 0 41 | return FONT_STYLES[isItalic] 42 | } 43 | 44 | export const getWeightOfFont = (font) => { 45 | const appKitWeight = NSFontManager.sharedFontManager().weightOfFont_(font) // eslint-disable-line 46 | return FONT_WEIGHTS[appKitWeight] 47 | } 48 | 49 | export const getLetterCasing = attrs => ( 50 | TEXT_TRANSFORM[attrs.MSAttributedStringTextTransformAttribute || 0] 51 | ) 52 | -------------------------------------------------------------------------------- /src/plugin/utils/helpers.js: -------------------------------------------------------------------------------- 1 | import { fromNative } from 'sketch/dom' 2 | 3 | export const __DEV__ = process.env.NODE_ENV !== 'production' // eslint-disable-line 4 | 5 | export const pluck = (arr, prop) => { 6 | const mapped = arr.map(item => item[prop]) 7 | return mapped.filter((value, index, self) => self.indexOf(value) === index) 8 | } 9 | 10 | export const getMSTextLayers = (selection) => { 11 | const predicate = NSPredicate.predicateWithFormat('className == %@', 'MSTextLayer') 12 | return selection.filteredArrayUsingPredicate(predicate) 13 | } 14 | 15 | export const getJSTextLayers = (selection) => { 16 | if (Array.isArray(selection)) { 17 | return selection.filter(layer => layer.type === 'Text') 18 | } 19 | 20 | const arr = [] 21 | getMSTextLayers(selection).forEach(layer => arr.push(fromNative(layer))) 22 | return arr 23 | } 24 | -------------------------------------------------------------------------------- /src/webview/client.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable react/jsx-filename-extension */ 2 | import React, { Fragment } from 'react' 3 | import { render } from 'react-dom' 4 | import { HashRouter } from 'react-router-dom' 5 | import { renderRoutes } from 'react-router-config' 6 | import pluginCall from 'sketch-module-web-view/client' 7 | import { Global } from '@emotion/core' 8 | import globals from 'webview/style/globals' 9 | import routes from 'webview/pages' 10 | 11 | const renderApp = (props) => { 12 | render( 13 | 14 | 15 | 16 | { renderRoutes(routes, props) } 17 | 18 | , 19 | document.getElementById('root') 20 | ) 21 | } 22 | 23 | // Disable the context menu to have a more native feel 24 | document.addEventListener('contextmenu', (e => ( 25 | e.preventDefault() 26 | ))) 27 | 28 | // Call the sketch plugin to get the current user preferences 29 | pluginCall('getPreferences') 30 | 31 | // Now render the app 32 | renderApp({ 33 | preferences: window.preferences 34 | }) 35 | 36 | // This is called from the plugin and tells the webview to refresh 37 | window.reloadData = (newProps) => { 38 | renderApp({ 39 | preferences: newProps 40 | }) 41 | } 42 | -------------------------------------------------------------------------------- /src/webview/components/Button/index.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import PropTypes from 'prop-types' 3 | import styled from '@emotion/styled' 4 | import { 5 | colors, 6 | fonts, 7 | radii, 8 | space 9 | } from 'webview/style/variables' 10 | 11 | const Base = styled.button` 12 | appearance: none; 13 | border: 0; 14 | padding: calc(${ space[1] } * 1.25) ${ space[2] }; 15 | margin: ${ space[1] } ${ space[1] } 0 0; 16 | background: ${ colors.black.base }; 17 | border-radius: ${ radii[2] }; 18 | 19 | ${ fonts.s10.n700 }; 20 | color: ${ colors.white.base }; 21 | text-align: center; 22 | 23 | &:hover { 24 | cursor: pointer; 25 | } 26 | ` 27 | 28 | const Button = ({ label, name, onClick }) => ( 29 | 34 | { label } 35 | 36 | ) 37 | 38 | Button.propTypes = { 39 | label: PropTypes.string.isRequired, 40 | name: PropTypes.string.isRequired, 41 | onClick: PropTypes.func.isRequired 42 | } 43 | 44 | export default Button 45 | -------------------------------------------------------------------------------- /src/webview/components/Checkbox/index.jsx: -------------------------------------------------------------------------------- 1 | import React, { PureComponent } from 'react' 2 | import PropTypes from 'prop-types' 3 | import Field from 'webview/components/Field' 4 | import { CheckboxInput, CheckboxLabel } from './styles' 5 | 6 | class Checkbox extends PureComponent { 7 | static propTypes = { 8 | onChange: PropTypes.func.isRequired, 9 | name: PropTypes.string.isRequired, 10 | checked: PropTypes.oneOfType([ 11 | PropTypes.bool, 12 | PropTypes.number 13 | ]), 14 | label: PropTypes.string 15 | } 16 | 17 | static defaultProps = { 18 | checked: true, 19 | label: null 20 | } 21 | 22 | constructor(props) { 23 | super(props) 24 | this.handleOnChange = this.handleOnChange.bind(this) 25 | } 26 | 27 | handleOnChange(evt) { 28 | const { onChange } = this.props 29 | onChange(evt.target) 30 | } 31 | 32 | render() { 33 | const { 34 | name, 35 | checked, 36 | label 37 | } = this.props 38 | 39 | return ( 40 | 41 | 48 | { label && ( 49 | 50 | { label } 51 | 52 | ) } 53 | 54 | ) 55 | } 56 | } 57 | 58 | export default Checkbox 59 | -------------------------------------------------------------------------------- /src/webview/components/Checkbox/styles.js: -------------------------------------------------------------------------------- 1 | import styled from '@emotion/styled' 2 | import { 3 | colors, 4 | fonts, 5 | radii, 6 | space 7 | } from 'webview/style/variables' 8 | 9 | export const CheckboxInput = styled.input` 10 | appearance: none; 11 | background: ${ colors.black.l100 }; 12 | border: 0; 13 | border-radius: ${ radii[1] }; 14 | box-shadow: inset 0 0 0 1px ${ colors.black.l92 }; 15 | height: 14px; 16 | margin: 0; 17 | width: 14px; 18 | 19 | &:checked { 20 | box-shadow: inset 0 0 0 1px ${ colors.black.base }; 21 | background: ${ colors.black.base }; 22 | outline: none; 23 | }, 24 | 25 | &:hover { 26 | cursor: pointer; 27 | } 28 | ` 29 | 30 | export const CheckboxLabel = styled.label` 31 | ${ fonts.s12.n400 }; 32 | color: ${ colors.black.l10 }; 33 | padding-left: ${ space[1] }; 34 | width: 100%; 35 | &:hover { 36 | cursor: pointer; 37 | } 38 | ` 39 | -------------------------------------------------------------------------------- /src/webview/components/Field/index.jsx: -------------------------------------------------------------------------------- 1 | import styled from '@emotion/styled' 2 | import { space } from 'webview/style/variables' 3 | 4 | const Field = styled.div` 5 | display: flex; 6 | align-items: center; 7 | flex-wrap: ${ props => (props.wrap ? 'wrap' : null) }; 8 | padding-top: ${ space[1] }; 9 | padding-bottom: ${ space[1] }; 10 | ` 11 | 12 | export default Field 13 | -------------------------------------------------------------------------------- /src/webview/components/Page/Footer/index.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import styled from '@emotion/styled' 3 | import pluginCall from 'sketch-module-web-view/client' 4 | import InlineSVG from 'svg-inline-react' 5 | import { colors, space } from 'webview/style/variables' 6 | import markSvg from './mark.svg' 7 | 8 | const Footer = styled.footer` 9 | background: ${ colors.black.l97 }; 10 | display: flex; 11 | align-items: center; 12 | justify-content: center; 13 | padding-top: ${ space[6] }; 14 | padding-left: ${ space[2] }; 15 | padding-right: ${ space[2] }; 16 | padding-bottom: ${ space[6] }; 17 | ` 18 | 19 | const Mark = styled(InlineSVG)` 20 | width: 24px; 21 | height: 24px; 22 | 23 | path { 24 | fill: ${ colors.black.l30 }; 25 | } 26 | 27 | &:hover { 28 | cursor: pointer; 29 | } 30 | ` 31 | 32 | const handleOnClickMark = () => ( 33 | pluginCall('openUrl', 'https://github.com/buames/typesettings-sketch-plugin') 34 | ) 35 | 36 | const PageFooter = () => ( 37 |
38 | 43 |
44 | ) 45 | 46 | export default PageFooter 47 | -------------------------------------------------------------------------------- /src/webview/components/Page/Footer/mark.svg: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /src/webview/components/Page/Header/index.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import PropTypes from 'prop-types' 3 | import styled from '@emotion/styled' 4 | import { 5 | borders, 6 | colors, 7 | fonts, 8 | space 9 | } from 'webview/style/variables' 10 | 11 | export const headerHeight = '48px' 12 | 13 | const Header = styled.header` 14 | background: ${ colors.white.base }; 15 | border-bottom: ${ borders[1] } ${ colors.black.l92 }; 16 | padding-left: ${ space[2] }; 17 | padding-right: ${ space[2] }; 18 | height: ${ headerHeight }; 19 | position: fixed; 20 | top: 0; 21 | left: 0; 22 | right: 0; 23 | display: flex; 24 | align-items: center; 25 | justify-content: center; 26 | ` 27 | 28 | const Title = styled.h1` 29 | ${ fonts.s14.n700 }; 30 | color: ${ colors.black.l10 }; 31 | ` 32 | 33 | const PageHeader = ({ title }) => ( 34 |
35 | 36 | { title } 37 | 38 |
39 | ) 40 | 41 | PageHeader.propTypes = { 42 | title: PropTypes.string 43 | } 44 | 45 | PageHeader.defaultProps = { 46 | title: null 47 | } 48 | 49 | export default PageHeader 50 | -------------------------------------------------------------------------------- /src/webview/components/Page/index.jsx: -------------------------------------------------------------------------------- 1 | import React, { Fragment } from 'react' 2 | import PropTypes from 'prop-types' 3 | import styled from '@emotion/styled' 4 | import { space } from 'webview/style/variables' 5 | import PageHeader, { headerHeight } from './Header' 6 | import PageFooter from './Footer' 7 | 8 | const Main = styled.main` 9 | padding-top: ${ headerHeight }; 10 | padding-left: ${ space[2] }; 11 | padding-right: ${ space[2] }; 12 | ` 13 | 14 | const Page = ({ title, children }) => ( 15 | 16 | 17 |
18 | { children } 19 |
20 | 21 |
22 | ) 23 | 24 | Page.propTypes = { 25 | children: PropTypes.node.isRequired, 26 | title: PropTypes.string 27 | } 28 | 29 | Page.defaultProps = { 30 | title: null 31 | } 32 | 33 | export default Page 34 | -------------------------------------------------------------------------------- /src/webview/components/TextField/index.jsx: -------------------------------------------------------------------------------- 1 | import React, { PureComponent } from 'react' 2 | import PropTypes from 'prop-types' 3 | import Field from 'webview/components/Field' 4 | import { Input, InputLabel } from './styles' 5 | 6 | class TextField extends PureComponent { 7 | static propTypes = { 8 | onChange: PropTypes.func.isRequired, 9 | name: PropTypes.string.isRequired, 10 | placeholder: PropTypes.string, 11 | value: PropTypes.string, 12 | label: PropTypes.string, 13 | disabled: PropTypes.bool 14 | } 15 | 16 | static defaultProps = { 17 | placeholder: null, 18 | value: null, 19 | label: null, 20 | disabled: false 21 | } 22 | 23 | constructor(props) { 24 | super(props) 25 | this.handleOnChange = this.handleOnChange.bind(this) 26 | } 27 | 28 | handleOnChange(evt) { 29 | const { onChange } = this.props 30 | onChange(evt.target) 31 | } 32 | 33 | render() { 34 | const { 35 | name, 36 | placeholder, 37 | value, 38 | label, 39 | disabled 40 | } = this.props 41 | 42 | return ( 43 | 44 | { label && ( 45 | 46 | { label } 47 | 48 | ) } 49 | 57 | 58 | ) 59 | } 60 | } 61 | 62 | export default TextField 63 | -------------------------------------------------------------------------------- /src/webview/components/TextField/styles.js: -------------------------------------------------------------------------------- 1 | import styled from '@emotion/styled' 2 | import { 3 | colors, 4 | fonts, 5 | radii, 6 | space 7 | } from 'webview/style/variables' 8 | 9 | export const Input = styled.input` 10 | appearance: none; 11 | background: ${ colors.white.base }; 12 | border: 0; 13 | border-radius: ${ radii[1] }; 14 | box-shadow: inset 0 0 0 1px ${ colors.black.l92 }; 15 | margin: 0; 16 | padding: ${ space[1] }; 17 | width: 100%; 18 | 19 | ${ fonts.s12.n400 }; 20 | color: ${ colors.black.l10 }; 21 | 22 | &:focus { 23 | box-shadow: inset 0 0 0 1px ${ colors.black.base }; 24 | outline: none; 25 | } 26 | 27 | &:disabled { 28 | background-color: ${ colors.black.l97 }; 29 | cursor: not-allowed; 30 | } 31 | 32 | &::placeholder { 33 | ${ fonts.s12.n400 }; 34 | color: ${ colors.black.l55 }; 35 | } 36 | ` 37 | 38 | export const InputLabel = styled.label` 39 | ${ fonts.s12.n400 }; 40 | color: ${ colors.black.l10 }; 41 | padding-top: ${ space[1] }; 42 | padding-bottom: ${ space[1] }; 43 | width: 100%; 44 | ` 45 | -------------------------------------------------------------------------------- /src/webview/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Typesettings 6 | 7 | 8 | 9 |
10 | 11 | 12 | 13 | -------------------------------------------------------------------------------- /src/webview/pages/SettingsPage/Section.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import PropTypes from 'prop-types' 3 | import styled from '@emotion/styled' 4 | import { 5 | colors, 6 | borders, 7 | fonts, 8 | space 9 | } from 'webview/style/variables' 10 | 11 | const Container = styled.section` 12 | padding-top: ${ space[3] }; 13 | padding-bottom: ${ space[3] }; 14 | border-top: ${ borders[1] } ${ colors.black.l92 }; 15 | 16 | &:first-of-type { 17 | border-top: none; 18 | } 19 | ` 20 | 21 | const TitleLabel = styled.h2` 22 | ${ fonts.s12.n700 }; 23 | color: ${ colors.black.l10 }; 24 | padding-bottom: ${ space[2] }; 25 | ` 26 | 27 | const Section = ({ children, title }) => ( 28 | 29 | { title && ( 30 | 31 | { title } 32 | 33 | ) } 34 | { children } 35 | 36 | ) 37 | 38 | Section.propTypes = { 39 | title: PropTypes.string, 40 | children: PropTypes.node.isRequired 41 | } 42 | 43 | Section.defaultProps = { 44 | title: null 45 | } 46 | 47 | export default Section 48 | -------------------------------------------------------------------------------- /src/webview/pages/SettingsPage/index.jsx: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react' 2 | import PropTypes from 'prop-types' 3 | import pluginCall from 'sketch-module-web-view/client' 4 | import Page from 'webview/components/Page' 5 | import Button from 'webview/components/Button' 6 | import Checkbox from 'webview/components/Checkbox' 7 | import TextField from 'webview/components/TextField' 8 | import Section from './Section' 9 | 10 | class SettingsPage extends Component { 11 | state = { 12 | ...this.props.preferences // eslint-disable-line 13 | } 14 | 15 | static propTypes = { 16 | route: PropTypes.objectOf(PropTypes.any).isRequired, 17 | preferences: PropTypes.shape({ 18 | pluginDefinedDirectory: PropTypes.string, 19 | userDefinedDirectory: PropTypes.string, 20 | allowsAutoKerning: PropTypes.oneOfType([ 21 | PropTypes.bool, 22 | PropTypes.number 23 | ]), 24 | allowsAutoLineHeight: PropTypes.bool, 25 | ignorePrefix: PropTypes.string, 26 | ignoreSuffix: PropTypes.string 27 | }).isRequired 28 | } 29 | 30 | static getDerivedStateFromProps = (props, state) => { 31 | if (props.preferences !== state.preferences) { 32 | return { ...props.preferences } 33 | } 34 | return null 35 | } 36 | 37 | handleOnChange = (target) => { 38 | const { name } = target 39 | const newPref = (target.type === 'checkbox') 40 | ? target.checked 41 | : target.value 42 | 43 | this.state[name] = newPref 44 | pluginCall('setPreferences', this.state) 45 | } 46 | 47 | handleOnClickDirectoryButton = (evt) => { 48 | pluginCall(evt.target.name, this.state) 49 | } 50 | 51 | render() { 52 | const { route } = this.props 53 | const { 54 | userDefinedDirectory, 55 | allowsAutoKerning, 56 | allowsAutoLineHeight, 57 | ignorePrefix, 58 | ignoreSuffix 59 | } = this.state 60 | return ( 61 | 62 |
63 | 70 |
86 |
87 | 93 | 99 |
100 |
101 | 108 | 115 |
116 |
117 | ) 118 | } 119 | } 120 | 121 | export default SettingsPage 122 | -------------------------------------------------------------------------------- /src/webview/pages/index.js: -------------------------------------------------------------------------------- 1 | import SettingsPage from './SettingsPage' 2 | 3 | const routes = [ 4 | { 5 | exact: true, 6 | path: '/', 7 | component: SettingsPage, 8 | title: 'Plugin Settings' 9 | } 10 | ] 11 | 12 | export default routes 13 | -------------------------------------------------------------------------------- /src/webview/style/globals/index.js: -------------------------------------------------------------------------------- 1 | import { css } from '@emotion/core' 2 | 3 | const globals = css` 4 | * { 5 | box-sizing: border-box; 6 | } 7 | 8 | html { 9 | line-height: 1.15; 10 | font-size: 100%; 11 | } 12 | 13 | html, 14 | body, 15 | h1, 16 | h2, 17 | h3, 18 | h4, 19 | h5, 20 | h6, 21 | p { 22 | margin: 0; 23 | padding: 0; 24 | } 25 | ` 26 | 27 | export default globals 28 | -------------------------------------------------------------------------------- /src/webview/style/variables/index.js: -------------------------------------------------------------------------------- 1 | import { generateFonts } from 'typesettings-js' 2 | import Typesettings from './typesettings' 3 | 4 | export const borders = [ 5 | 0, 6 | '1px solid', 7 | '2px solid' 8 | ] 9 | 10 | export const colors = { 11 | black: { 12 | base: '#000000', 13 | l10: '#191919', 14 | l55: '#8C8C8C', 15 | l30: '#B2B2B2', 16 | l92: '#EBEBEB', 17 | l97: '#F7F7F7', 18 | l100: '#FFFFFF' 19 | }, 20 | white: { 21 | base: '#FFFFFF', 22 | l10: '#E6E6E6', 23 | l55: '#737373', 24 | l30: '#4D4D4D', 25 | l92: '#141414', 26 | l97: '#080808', 27 | l100: '#000000' 28 | }, 29 | brand: { 30 | base: '#03A87C' 31 | } 32 | } 33 | 34 | export const fonts = generateFonts(Typesettings) 35 | 36 | export const radii = [ 37 | 0, 38 | '3px', 39 | '5px', 40 | '10px', 41 | '1000px' 42 | ] 43 | 44 | export const space = [ 45 | '0px', 46 | '8px', 47 | '16px', 48 | '24px', 49 | '32px', 50 | '40px', 51 | '48px' 52 | ] 53 | 54 | export default { 55 | borders, 56 | colors, 57 | fonts, 58 | radii, 59 | space 60 | } 61 | -------------------------------------------------------------------------------- /src/webview/style/variables/typesettings.js: -------------------------------------------------------------------------------- 1 | const typesettings = { 2 | family: '-apple-system', 3 | fallback: "BlinkMacSystemFont, 'Helvetica Neue', 'Arial', sans-serif", 4 | regular: { 5 | fontStyle: 'normal', 6 | fontWeight: 400, 7 | normalcase: { 8 | 10: { 9 | characterSpacing: 0, 10 | lineHeight: null, 11 | paragraphSpacing: 0 12 | }, 13 | 12: { 14 | characterSpacing: -0.07999999821186066, 15 | lineHeight: 18, 16 | paragraphSpacing: 0 17 | }, 18 | 14: { 19 | characterSpacing: -0.1500000059604645, 20 | lineHeight: 19, 21 | paragraphSpacing: 0 22 | } 23 | } 24 | }, 25 | medium: { 26 | fontStyle: 'normal', 27 | fontWeight: 500, 28 | normalcase: { 29 | 10: { 30 | characterSpacing: 0, 31 | lineHeight: null, 32 | paragraphSpacing: 0 33 | }, 34 | 12: { 35 | characterSpacing: -0.07999999821186066, 36 | lineHeight: null, 37 | paragraphSpacing: 0 38 | }, 39 | 14: { 40 | characterSpacing: -0.1500000059604645, 41 | lineHeight: null, 42 | paragraphSpacing: 0 43 | } 44 | } 45 | }, 46 | bold: { 47 | fontStyle: 'normal', 48 | fontWeight: 700, 49 | normalcase: { 50 | 10: { 51 | characterSpacing: 0, 52 | lineHeight: null, 53 | paragraphSpacing: 0 54 | }, 55 | 12: { 56 | characterSpacing: -0.07999999821186066, 57 | lineHeight: null, 58 | paragraphSpacing: 0 59 | }, 60 | 14: { 61 | characterSpacing: -0.1500000059604645, 62 | lineHeight: null, 63 | paragraphSpacing: 0 64 | } 65 | } 66 | } 67 | } 68 | 69 | export default typesettings 70 | -------------------------------------------------------------------------------- /webpack.skpm.config.js: -------------------------------------------------------------------------------- 1 | const path = require('path') 2 | 3 | const context = path.resolve(process.cwd(), 'src') 4 | 5 | module.exports = (config, isPluginCommand) => { 6 | config.context = context 7 | config.resolve.modules.push(context) 8 | 9 | if (!isPluginCommand) { 10 | config.resolve.extensions.push('.json', '.jsx') 11 | config.module.rules.push({ 12 | test: /\.(html)$/, 13 | use: [ 14 | { loader: '@skpm/extract-loader' }, 15 | { 16 | loader: 'html-loader', 17 | options: { 18 | attrs: [ 19 | 'img:src', 20 | 'link:href' 21 | ], 22 | interpolate: true 23 | } 24 | } 25 | ] 26 | }) 27 | config.module.rules.push({ 28 | test: /\.svg$/, 29 | use: [ 30 | { loader: 'svg-inline-loader' } 31 | ] 32 | }) 33 | } 34 | } 35 | --------------------------------------------------------------------------------