├── .babelrc ├── .gitignore ├── resources ├── script.js └── webview │ ├── components │ ├── common │ │ ├── Canvas.jsx │ │ ├── Border.jsx │ │ ├── Container.jsx │ │ ├── Swatch.jsx │ │ ├── ColorModes.jsx │ │ ├── ColorBox.jsx │ │ └── ColorInput.jsx │ ├── GradientView.jsx │ └── ColorScaleView.jsx │ ├── index.jsx │ └── helpers.js ├── webpack.skpm.config.js ├── chromatic-sketch.sketchplugin └── Contents │ ├── Resources │ ├── index.html │ ├── style.css │ └── rc-slider.css │ └── Sketch │ ├── manifest.json │ ├── gradient.js │ └── color-scale.js ├── LICENSE.md ├── src ├── manifest.json ├── helpers.js ├── gradient.js └── color-scale.js ├── package.json ├── appcast.xml └── README.md /.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "plugins": ["transform-class-properties"] 3 | } 4 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # npm 2 | node_modules 3 | .npm 4 | npm-debug.log 5 | 6 | # mac 7 | .DS_Store -------------------------------------------------------------------------------- /resources/script.js: -------------------------------------------------------------------------------- 1 | import './webview' 2 | 3 | //document.addEventListener('contextmenu', e => e.preventDefault()) 4 | -------------------------------------------------------------------------------- /webpack.skpm.config.js: -------------------------------------------------------------------------------- 1 | const webpack = require('webpack') 2 | 3 | module.exports = config => { 4 | config.resolve.extensions = ['.js', '.jsx'] 5 | } 6 | -------------------------------------------------------------------------------- /chromatic-sketch.sketchplugin/Contents/Resources/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Chromatic Sketch 6 | 7 | 8 | 9 | 10 |
11 | 12 | 13 | -------------------------------------------------------------------------------- /resources/webview/components/common/Canvas.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import { StyleSheet, css } from 'aphrodite' 3 | 4 | class Canvas extends React.PureComponent { 5 | render() { 6 | return
{this.props.children}
7 | } 8 | } 9 | 10 | const styles = StyleSheet.create({ 11 | canvas: { 12 | background: '#fff', 13 | borderTop: '1px solid #DFDFDF', 14 | borderBottom: '1px solid #DFDFDF', 15 | padding: 40, 16 | }, 17 | }) 18 | 19 | export default Canvas 20 | -------------------------------------------------------------------------------- /resources/webview/components/common/Border.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import { StyleSheet, css } from 'aphrodite' 3 | 4 | class Border extends React.PureComponent { 5 | render() { 6 | return
{this.props.children}
7 | } 8 | } 9 | 10 | const styles = StyleSheet.create({ 11 | border: { 12 | position: 'relative', 13 | '::after': { 14 | content: '""', 15 | position: 'absolute', 16 | left: 0, 17 | top: 0, 18 | right: 0, 19 | bottom: 0, 20 | border: '1px solid rgba(0,0,0,0.07)', 21 | borderRadius: 3, 22 | pointerEvents: 'none', 23 | }, 24 | }, 25 | }) 26 | 27 | export default Border 28 | -------------------------------------------------------------------------------- /resources/webview/index.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import ReactDOM from 'react-dom' 3 | import ColorScaleView from './components/ColorScaleView' 4 | import GradientView from './components/GradientView' 5 | import pluginCall from 'sketch-module-web-view/client' 6 | 7 | window.renderColorScaleView = (firstColor, lastColor) => { 8 | ReactDOM.render( 9 | , 10 | document.getElementById('container') 11 | ) 12 | } 13 | 14 | window.renderGradientView = (colorArray, positionArray) => { 15 | ReactDOM.render( 16 | , 17 | document.getElementById('container') 18 | ) 19 | } 20 | 21 | pluginCall('ready') 22 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | The ISC License 2 | 3 | Copyright 2018 Petter Nilsson 4 | 5 | Permission to use, copy, modify, and/or distribute this software for any purpose with or without fee is hereby granted, provided that the above copyright notice and this permission notice appear in all copies. 6 | 7 | THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. 8 | -------------------------------------------------------------------------------- /src/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "author" : "Petter Nilsson", 3 | "authorEmail" : "petterheterjag@gmail.com", 4 | "identifier": "com.sketchapp.plugins.chromatic-sketch", 5 | "appcast": "https://github.com/petterheterjag/chromatic-sketch/blob/master/appcast.xml?raw=true", 6 | "compatibleVersion": 3, 7 | "bundleVersion": 1, 8 | "commands": [ 9 | { 10 | "name": "Create Color Scale", 11 | "identifier": "color-scale", 12 | "script": "./color-scale.js" 13 | }, 14 | { 15 | "name": "Fix Gradient", 16 | "identifier": "gradient", 17 | "script": "./gradient.js" 18 | } 19 | ], 20 | "menu": { 21 | "title": "Chromatic Sketch", 22 | "items": [ 23 | "color-scale", 24 | "gradient" 25 | ] 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /resources/webview/components/common/Container.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import { StyleSheet, css } from 'aphrodite' 3 | 4 | class Container extends React.PureComponent { 5 | static defaultProps = { 6 | rightAlign: false, 7 | } 8 | 9 | render() { 10 | return ( 11 |
17 | {this.props.children} 18 |
19 | ) 20 | } 21 | } 22 | 23 | const styles = StyleSheet.create({ 24 | container: { 25 | padding: 20, 26 | display: 'flex', 27 | justifyContent: 'flex-start', 28 | }, 29 | rightAlign: { 30 | justifyContent: 'flex-end', 31 | }, 32 | }) 33 | 34 | export default Container 35 | -------------------------------------------------------------------------------- /chromatic-sketch.sketchplugin/Contents/Resources/style.css: -------------------------------------------------------------------------------- 1 | body { 2 | background-color: #F2F2F2; 3 | font-family: -apple-system, BlinkMacSystemFont, sans-serif; 4 | font-size: 13px; 5 | margin: 0; 6 | -webkit-font-smoothing: antialiased; 7 | } 8 | 9 | input[type="text"], input[type="number"] { 10 | border: 1px solid #d2d2d2; 11 | } 12 | 13 | .rc-slider { 14 | padding: 0; 15 | } 16 | 17 | .rc-slider-rail { 18 | height: 20px; 19 | border-radius: 3px; 20 | box-shadow: inset 0 0 0 1px rgba(0,0,0,0.07); 21 | } 22 | 23 | .rc-slider-track { 24 | opacity: 0; 25 | } 26 | 27 | .rc-slider-handle { 28 | box-shadow: 0 1px 2px 0 rgba(0,0,0,0.50); 29 | border: 0; 30 | border-radius: 2px; 31 | height: 20px; 32 | margin-top: 0; 33 | } 34 | 35 | .rc-slider-handle:active, .rc-slider-handle:focus { 36 | box-shadow: 0 1px 5px rgba(0,0,0,0.50); 37 | } 38 | -------------------------------------------------------------------------------- /resources/webview/components/common/Swatch.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import chroma from 'chroma-js' 3 | import { StyleSheet, css } from 'aphrodite' 4 | import { getTextColor } from '../../helpers' 5 | import ColorBox from './ColorBox' 6 | 7 | class Swatch extends React.PureComponent { 8 | render() { 9 | return ( 10 |
11 | 12 | 13 | {this.props.color.hex()} 14 | 15 | 16 |
17 | ) 18 | } 19 | } 20 | 21 | const styles = StyleSheet.create({ 22 | swatch: { 23 | flex: '1 0 0', 24 | position: 'relative', 25 | fontSize: 11, 26 | textTransform: 'uppercase', 27 | }, 28 | }) 29 | 30 | export default Swatch 31 | -------------------------------------------------------------------------------- /resources/webview/components/common/ColorModes.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import { StyleSheet, css } from 'aphrodite' 3 | 4 | class ColorModes extends React.PureComponent { 5 | render() { 6 | return ( 7 |
8 | 21 |
22 | ) 23 | } 24 | } 25 | 26 | const styles = StyleSheet.create({ 27 | layout: { 28 | marginLeft: 'auto', 29 | }, 30 | select: { 31 | fontSize: 16, 32 | marginLeft: 10, 33 | marginTop: 9, 34 | }, 35 | }) 36 | 37 | export default ColorModes 38 | -------------------------------------------------------------------------------- /resources/webview/helpers.js: -------------------------------------------------------------------------------- 1 | import chroma from 'chroma-js' 2 | 3 | const GRADIENT_STOPS = 5 4 | 5 | export function interpolateArray(array) { 6 | let interpolatedArray = [] 7 | 8 | for (let i = 0; i < array.length - 1; i++) { 9 | const difference = array[i + 1] - array[i] 10 | const interpolatedDifference = difference / GRADIENT_STOPS 11 | 12 | for (let j = 0; j <= GRADIENT_STOPS; j++) { 13 | interpolatedArray.push(array[i] + interpolatedDifference * j) 14 | } 15 | } 16 | 17 | return interpolatedArray 18 | } 19 | 20 | export function createGradientStyle(scale, positionArray = [0, 1]) { 21 | let colorArray = [] 22 | interpolateArray(positionArray).forEach(function(position) { 23 | colorArray.push(scale(position).css()) 24 | }) 25 | return `linear-gradient(to right, ${colorArray.join()})` 26 | } 27 | 28 | export function getTextColor(color) { 29 | if (chroma.contrast(color, '#fff') * color.alpha() < 2) { 30 | return '#000' 31 | } else { 32 | return '#fff' 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /chromatic-sketch.sketchplugin/Contents/Sketch/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "author": "Petter Nilsson", 3 | "authorEmail": "petterheterjag@gmail.com", 4 | "identifier": "com.sketchapp.plugins.chromatic-sketch", 5 | "appcast": "https://github.com/petterheterjag/chromatic-sketch/blob/master/appcast.xml?raw=true", 6 | "compatibleVersion": 3, 7 | "bundleVersion": 1, 8 | "commands": [ 9 | { 10 | "name": "Create Color Scale", 11 | "identifier": "color-scale", 12 | "script": "color-scale.js" 13 | }, 14 | { 15 | "name": "Fix Gradient", 16 | "identifier": "gradient", 17 | "script": "gradient.js" 18 | } 19 | ], 20 | "menu": { 21 | "title": "Chromatic Sketch", 22 | "items": [ 23 | "color-scale", 24 | "gradient" 25 | ] 26 | }, 27 | "version": "2.0.0", 28 | "description": "Create good-looking and perceptually uniform gradients and color scales (using Chroma.js and the Lab color space)", 29 | "homepage": "http://petter.pro", 30 | "name": "chromatic-sketch", 31 | "disableCocoaScriptPreprocessor": true 32 | } -------------------------------------------------------------------------------- /src/helpers.js: -------------------------------------------------------------------------------- 1 | import WebUI from 'sketch-module-web-view' 2 | 3 | export function makeColor(c) { 4 | return MSImmutableColor.colorWithRed_green_blue_alpha( 5 | c[0] / 255, 6 | c[1] / 255, 7 | c[2] / 255, 8 | c[3] 9 | ).newMutableCounterpart() 10 | } 11 | 12 | export function buildDialog(message, informativeText) { 13 | var alert = COSAlertWindow.new() 14 | alert.setMessageText(message) 15 | alert.setInformativeText(informativeText) 16 | return alert 17 | } 18 | 19 | export function createWebview(context, handlers, title, height) { 20 | const v = 242 / 255 21 | const grayColor = NSColor.colorWithRed_green_blue_alpha(v, v, v, 1) 22 | let options = { 23 | identifier: 'unique.id', 24 | x: 0, 25 | y: 0, 26 | width: 630, 27 | height: height, 28 | background: grayColor, 29 | blurredBackground: false, 30 | onlyShowCloseButton: false, 31 | title: title, 32 | hideTitleBar: false, 33 | shouldKeepAround: true, 34 | resizable: false, 35 | handlers: handlers, 36 | } 37 | return new WebUI(context, 'index.html', options) 38 | } 39 | -------------------------------------------------------------------------------- /resources/webview/components/common/ColorBox.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import { StyleSheet, css } from 'aphrodite' 3 | import { Checkboard } from 'react-color/lib/components/common' 4 | 5 | class ColorBox extends React.PureComponent { 6 | render() { 7 | const checkboardSize = this.props.height < 30 ? 6 : 12 8 | 9 | return ( 10 |
14 | 15 |
21 | {this.props.children} 22 |
23 |
24 | ) 25 | } 26 | } 27 | 28 | const styles = StyleSheet.create({ 29 | box: { 30 | position: 'relative', 31 | }, 32 | color: { 33 | position: 'absolute', 34 | left: 0, 35 | top: 0, 36 | right: 0, 37 | bottom: 0, 38 | display: 'flex', 39 | justifyContent: 'center', 40 | flexDirection: 'column', 41 | textAlign: 'center', 42 | }, 43 | }) 44 | 45 | export default ColorBox 46 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "chromatic-sketch", 3 | "version": "2.0.0", 4 | "description": "Create good-looking and perceptually uniform gradients and color scales (using Chroma.js and the Lab color space)", 5 | "engines": { 6 | "sketch": ">=3.0" 7 | }, 8 | "skpm": { 9 | "name": "chromatic-sketch", 10 | "manifest": "src/manifest.json", 11 | "main": "chromatic-sketch.sketchplugin" 12 | }, 13 | "resources": [ 14 | "resources/script.js" 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 | }, 22 | "author": "Petter Nilsson", 23 | "homepage": "http://petter.pro", 24 | "license": "ISC", 25 | "devDependencies": { 26 | "babel-plugin-transform-class-properties": "^6.24.1", 27 | "@skpm/builder": "^0.1.3", 28 | "@skpm/extract-loader": "^1.0.1" 29 | }, 30 | "dependencies": { 31 | "aphrodite": "^1.2.5", 32 | "chroma-js": "^1.3.3", 33 | "rc-slider": "^8.3.1", 34 | "react": "^16.0.0", 35 | "react-color": "^2.13.8", 36 | "react-desktop": "^0.3.1", 37 | "react-dom": "^16.0.0", 38 | "sketch-module-web-view": "^0.2.6" 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /src/gradient.js: -------------------------------------------------------------------------------- 1 | import { createWebview, makeColor, buildDialog } from './helpers' 2 | 3 | function makeStop(position, color) { 4 | return MSGradientStop.stopWithPosition_color_(position, makeColor(color)) 5 | } 6 | 7 | export default function(context) { 8 | if (context.selection.length == 0) { 9 | buildDialog( 10 | 'Fix Gradient', 11 | 'Select a shape with a gradient first' 12 | ).runModal() 13 | return 14 | } 15 | 16 | const selected = context.selection[0] 17 | const gradient = selected 18 | .style() 19 | .fills() 20 | .firstObject() 21 | .gradient() 22 | let positionArray = [] 23 | let colorArray = [] 24 | 25 | for (let i = 0; i < gradient.stops().length; i++) { 26 | const stop = gradient.stops()[i] 27 | colorArray.push( 28 | String( 29 | stop 30 | .color() 31 | .immutableModelObject() 32 | .stringValueWithAlpha(true) 33 | ) 34 | ) 35 | positionArray.push(stop.position()) 36 | } 37 | 38 | const handlers = { 39 | ready: function() { 40 | webview.eval( 41 | `window.renderGradientView(${JSON.stringify( 42 | colorArray 43 | )}, ${JSON.stringify(positionArray)})` 44 | ) 45 | }, 46 | applyGradient: function(stopArray) { 47 | let sketchStopArray = [] 48 | stopArray.forEach(function(stop) { 49 | sketchStopArray.push(makeStop(stop.position, stop.color)) 50 | }) 51 | gradient.setStops(sketchStopArray) 52 | webview.close() 53 | }, 54 | } 55 | 56 | const webview = createWebview(context, handlers, 'Fix Gradient', 410) 57 | } 58 | -------------------------------------------------------------------------------- /src/color-scale.js: -------------------------------------------------------------------------------- 1 | import { createWebview, makeColor } from './helpers' 2 | 3 | const PALETTE_WIDTH = 550 4 | const PALETTE_HEIGHT = 100 5 | 6 | export default function(context) { 7 | const sketch = context.api() 8 | const document = sketch.selectedDocument 9 | const selection = document.selectedLayers 10 | const page = document.selectedPage 11 | let layerColors = [] 12 | 13 | selection.iterate(layer => { 14 | if (layer.isShape) { 15 | layerColors.push( 16 | String( 17 | layer.style.sketchObject 18 | .fills() 19 | .firstObject() 20 | .color() 21 | .immutableModelObject() 22 | .stringValueWithAlpha(true) 23 | ) 24 | ) 25 | } 26 | }) 27 | 28 | const handlers = { 29 | ready: function() { 30 | webview.eval( 31 | `window.renderColorScaleView('${layerColors[0] || 32 | '#dddddd'}', '${layerColors[1] || '#000000'}')` 33 | ) 34 | }, 35 | insert: function(colorArray) { 36 | webview.close() 37 | const group = page.newGroup({ 38 | frame: new sketch.Rectangle(0, 0, PALETTE_WIDTH, PALETTE_HEIGHT), 39 | name: 'Color Scale', 40 | }) 41 | const swatchWidth = PALETTE_WIDTH / colorArray.length 42 | 43 | for (let i = 0; i < colorArray.length; i++) { 44 | const myStyle = new sketch.Style() 45 | myStyle.borders = '' 46 | myStyle.sketchObject 47 | .fills() 48 | .firstObject() 49 | .setColor(makeColor(colorArray[i])) 50 | const rect = group.newShape({ 51 | frame: new sketch.Rectangle(swatchWidth * i, 0, swatchWidth, 100), 52 | style: myStyle, 53 | }) 54 | } 55 | }, 56 | } 57 | 58 | const webview = createWebview(context, handlers, 'Create Color Scale', 435) 59 | } 60 | -------------------------------------------------------------------------------- /appcast.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Chromatic Sketch 5 | https://github.com/petterheterjag/chromatic-sketch/blob/master/appcast.xml?raw=true 6 | Create good-looking and perceptually uniform gradients and color scales. 7 | en 8 | 9 | Version 2.0.0 10 | 11 | 13 |
  • A proper UI that let's you preview and customize the gradient or color scale
  • 14 |
  • You can now use the Lch, HSL and RGB color modes in addition to Lab
  • 15 |
  • Support for colors with alpha
  • 16 | 17 | ]]> 18 |
    19 | 20 |
    21 | 22 | Version 1.0.2 23 | 24 | 26 |
  • Added support for Sketch's new system for updating plugins
  • 27 | 28 | ]]> 29 |
    30 | 31 |
    32 | 33 | Version 1.0.1 34 | 35 | 37 |
  • Now supports gradients with more than two colors (was a bug)
  • 38 | 39 | ]]> 40 |
    41 | 42 |
    43 |
    44 |
    -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Chromatic Sketch 2 | 3 | Create good-looking and perceptually uniform gradients and color scales (using [Chroma.js](https://github.com/gka/chroma.js) and the Lab color space) 4 | 5 | ![intro](https://cloud.githubusercontent.com/assets/1426460/25557981/a0e96b40-2d1d-11e7-9a5a-0bd3cbbdc746.png) 6 | 7 | ## New in version 2.0.0 8 | - A proper UI that let's you preview and customize the gradient or color scale 9 | - You can now use the Lch, HSL and RGB color modes in addition to Lab 10 | - Support for colors with alpha 11 | 12 | ## Background 13 | I came across [this](https://blog.bugsnag.com/chromatic-sass/) blog post recently. It opened my eyes to the [Lab color space](https://en.wikipedia.org/wiki/Lab_color_space), and how you can use it to create perceptually uniform gradients and color scales with SASS. Chroma.js is the underlying library powering it. Check it out if you want a deeper understanding of the Lab color space and why it's good for creating color scales. Basically, it's a color space that, unlike RGB, was built to mirror the visual response of the human eye. That makes it very well suited for interpolating colors. 14 | 15 | I thought this technique would be useful in design tools as well, and was kind of surprised that I couldn't find any Sketch plugins that implemented it. So I created this :) 16 | 17 | ## Usage 18 | #### Chromatic Sketch -> Fix Gradient 19 | This command will take the gradient of the selected shape and add new color stops to create a more aesthetically pleasing one. 20 | 21 | ![Fix Gradient](https://user-images.githubusercontent.com/1426460/33186103-4ff74096-d087-11e7-940d-0ee41190aab4.png) 22 | 23 | #### Chromatic Sketch -> Create Color Scale 24 | This command will create a scale between the fill colors of two selected shapes. 25 | 26 | ![Create Color Scale](https://user-images.githubusercontent.com/1426460/33186102-4e2d8734-d087-11e7-8299-356ecbe83b58.png) 27 | 28 | ## Install instructions 29 | 1. [Download .zip](https://github.com/petterheterjag/chromatic-sketch/archive/master.zip) 30 | 2. Extract contents 31 | 3. Navigate into the extracted folder and open chromatic-sketch.sketchplugin 32 | 4. Follow the on-screen prompts 33 | 34 | 35 | ## Building from source 36 | 1. Install dependencies: `npm install` 37 | 2. Build plugin: `npm run build` 38 | 39 | ## Created by 40 | Petter Nilsson 41 | [Twitter](https://twitter.com/petterheterjag) 42 | [Website](http://petter.pro) 43 | 44 | ## License 45 | ISC 46 | -------------------------------------------------------------------------------- /resources/webview/components/GradientView.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import pluginCall from 'sketch-module-web-view/client' 3 | import chroma from 'chroma-js' 4 | import { StyleSheet, css } from 'aphrodite' 5 | import { Button } from 'react-desktop/macOs' 6 | import { 7 | createGradientStyle, 8 | interpolateArray, 9 | getTextColor, 10 | } from '../helpers' 11 | 12 | import ColorModes from './common/ColorModes' 13 | import Container from './common/Container' 14 | import Canvas from './common/Canvas' 15 | import ColorBox from './common/ColorBox' 16 | import Border from './common/Border' 17 | 18 | class GradientView extends React.Component { 19 | constructor(props) { 20 | super() 21 | this.handleColorModeChange = this.handleColorModeChange.bind(this) 22 | this.handleApplyClick = this.handleApplyClick.bind(this) 23 | this.state = { 24 | colorMode: 'lab' 25 | } 26 | } 27 | 28 | handleColorModeChange(e) { 29 | this.setState({ colorMode: e.target.value }) 30 | } 31 | 32 | handleApplyClick() { 33 | const scale = chroma 34 | .scale(this.props.colorArray) 35 | .domain(this.props.positionArray) 36 | .mode(this.state.colorMode) 37 | let stopArray = [] 38 | 39 | interpolateArray(this.props.positionArray).forEach(function(position) { 40 | stopArray.push({ color: scale(position).rgba(), position: position }) 41 | }) 42 | 43 | pluginCall('applyGradient', stopArray) 44 | } 45 | 46 | render() { 47 | const newScale = chroma 48 | .scale(this.props.colorArray) 49 | .domain(this.props.positionArray) 50 | .mode(this.state.colorMode) 51 | const oldScale = chroma 52 | .scale(this.props.colorArray) 53 | .domain(this.props.positionArray) 54 | .mode('rgb') 55 | 56 | return ( 57 |
    58 | 59 | 63 | 64 | 65 | 66 | 71 | New 72 | 73 | 74 | 75 | 80 | Old 81 | 82 | 83 | 84 | 85 | 88 | 89 |
    90 | ) 91 | } 92 | } 93 | 94 | const styles = StyleSheet.create({ 95 | gradient: { 96 | borderRadius: 3, 97 | overflow: 'hidden', 98 | fontSize: 11, 99 | textTransform: 'uppercase', 100 | fontWeight: 700, 101 | }, 102 | new: { 103 | marginBottom: 4, 104 | }, 105 | }) 106 | 107 | export default GradientView 108 | -------------------------------------------------------------------------------- /resources/webview/components/common/ColorInput.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import { SketchPicker } from 'react-color' 3 | import { StyleSheet, css } from 'aphrodite' 4 | import chroma from 'chroma-js' 5 | import ColorBox from './ColorBox' 6 | import Border from './Border' 7 | 8 | class ColorInput extends React.PureComponent { 9 | constructor(props) { 10 | super() 11 | this.handleClick = this.handleClick.bind(this) 12 | this.handleClose = this.handleClose.bind(this) 13 | this.handleChange = this.handleChange.bind(this) 14 | this.state = { 15 | showPicker: false, 16 | } 17 | } 18 | 19 | createColorObject(chromaColor) { 20 | const rgba = chromaColor.rgba() 21 | return { 22 | r: rgba[0], 23 | g: rgba[1], 24 | b: rgba[2], 25 | a: rgba[3], 26 | } 27 | } 28 | 29 | handleClick() { 30 | this.setState({ showPicker: true }) 31 | } 32 | 33 | handleClose() { 34 | this.setState({ showPicker: false }) 35 | } 36 | 37 | handleChange(colorObject) { 38 | const color = colorObject.rgb 39 | const colorArray = [color.r, color.g, color.b, color.a] 40 | this.props.onChange(chroma(colorArray)) 41 | } 42 | 43 | render() { 44 | return ( 45 |
    48 |
    49 | 50 | 51 | 52 |
    53 | 59 |
    60 | {this.state.showPicker && ( 61 |
    62 |
    63 |
    64 | 69 |
    70 |
    71 | )} 72 |
    73 | ) 74 | } 75 | } 76 | 77 | const styles = StyleSheet.create({ 78 | layout: { 79 | marginRight: 10, 80 | position: 'relative', 81 | }, 82 | swatch: { 83 | position: 'absolute', 84 | width: 21, 85 | height: 21, 86 | marginTop: 9, 87 | marginLeft: 9, 88 | borderRadius: 3, 89 | overflow: 'hidden', 90 | }, 91 | input: { 92 | height: 34, 93 | paddingLeft: 38, 94 | width: 80, 95 | textTransform: 'uppercase', 96 | fontSize: 13, 97 | }, 98 | target: { 99 | position: 'absolute', 100 | top: 0, 101 | right: 0, 102 | bottom: 0, 103 | left: 0, 104 | zIndex: 9997, 105 | cursor: 'pointer', 106 | }, 107 | cover: { 108 | position: 'fixed', 109 | top: 0, 110 | right: 0, 111 | bottom: 0, 112 | left: 0, 113 | zIndex: 9998, 114 | }, 115 | popover: { 116 | position: 'absolute', 117 | zIndex: 9999, 118 | top: 46, 119 | left: -3, 120 | }, 121 | focused: { 122 | '::after': { 123 | content: '""', 124 | width: 'calc(100% + 6px)', 125 | height: 'calc(100% + 6px)', 126 | display: 'block', 127 | background: '#B9D8F7', 128 | position: 'absolute', 129 | left: -3, 130 | top: -3, 131 | zIndex: -1, 132 | borderRadius: 3, 133 | }, 134 | }, 135 | }) 136 | 137 | export default ColorInput 138 | -------------------------------------------------------------------------------- /resources/webview/components/ColorScaleView.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import pluginCall from 'sketch-module-web-view/client' 3 | import chroma from 'chroma-js' 4 | import { StyleSheet, css } from 'aphrodite' 5 | import { createGradientStyle } from '../helpers' 6 | import Slider from 'rc-slider' 7 | import { Button } from 'react-desktop/macOs' 8 | import { Checkboard } from 'react-color/lib/components/common' 9 | 10 | import ColorModes from './common/ColorModes' 11 | import ColorInput from './common/ColorInput' 12 | import Swatch from './common/Swatch' 13 | import Container from './common/Container' 14 | import Canvas from './common/Canvas' 15 | import Border from './common/Border' 16 | 17 | const createSliderWithTooltip = Slider.createSliderWithTooltip 18 | const Range = createSliderWithTooltip(Slider.Range) 19 | 20 | class ColorScaleView extends React.PureComponent { 21 | constructor(props) { 22 | super(props) 23 | this.handleFirstColorChange = this.handleFirstColorChange.bind(this) 24 | this.handleLastColorChange = this.handleLastColorChange.bind(this) 25 | this.handleNumberChange = this.handleNumberChange.bind(this) 26 | this.handleRangeChange = this.handleRangeChange.bind(this) 27 | this.handleColorModeChange = this.handleColorModeChange.bind(this) 28 | this.handleResetClick = this.handleResetClick.bind(this) 29 | this.handleInsertClick = this.handleInsertClick.bind(this) 30 | this.state = { 31 | firstColor: chroma(this.props.firstColor), 32 | lastColor: chroma(this.props.lastColor), 33 | scaleValues: [0, 33.3, 66.6, 100], 34 | defaultScale: true, 35 | colorMode: 'lab', 36 | } 37 | } 38 | 39 | createDefaultScale(count) { 40 | this.setState({ defaultScale: true }) 41 | let defaultScale = [] 42 | for (var i = 0; i < count; i++) { 43 | defaultScale.push(100 / (count - 1) * i) 44 | } 45 | 46 | return defaultScale 47 | } 48 | 49 | handleRangeChange(value) { 50 | this.setState({ scaleValues: value }) 51 | this.setState({ defaultScale: false }) 52 | } 53 | 54 | handleFirstColorChange(color) { 55 | this.setState({ firstColor: color }) 56 | } 57 | 58 | handleLastColorChange(color) { 59 | this.setState({ lastColor: color }) 60 | } 61 | 62 | handleNumberChange(e) { 63 | let number = Number(e.target.value) 64 | if (number < 2) { 65 | number = 2 66 | } else if (number > 9) { 67 | number = 9 68 | } 69 | 70 | this.setState({ scaleValues: this.createDefaultScale(number) }) 71 | } 72 | 73 | handleColorModeChange(e) { 74 | this.setState({ colorMode: e.target.value }) 75 | } 76 | 77 | handleResetClick() { 78 | this.setState({ 79 | scaleValues: this.createDefaultScale(this.state.scaleValues.length), 80 | }) 81 | } 82 | 83 | handleInsertClick() { 84 | const scale = chroma 85 | .scale([this.state.firstColor, this.state.lastColor]) 86 | .mode(this.state.colorMode) 87 | var colorArray = [] 88 | for (var i = 0; i < this.state.scaleValues.length; i++) { 89 | colorArray.push(scale(this.state.scaleValues[i] / 100).rgba()) 90 | } 91 | 92 | pluginCall('insert', colorArray) 93 | } 94 | 95 | render() { 96 | const scale = chroma 97 | .scale([this.state.firstColor, this.state.lastColor]) 98 | .mode(this.state.colorMode) 99 | const numberOfSwatches = this.state.scaleValues.length 100 | const gradientStyle = createGradientStyle(scale) 101 | 102 | var swatchArray = [] 103 | for (var i = 0; i < numberOfSwatches; i++) { 104 | swatchArray.push( 105 | 106 | ) 107 | } 108 | 109 | return ( 110 |
    111 | 112 | 116 | 120 | 128 | 132 | 133 | 134 | 135 |
    {swatchArray}
    136 |
    137 |
    138 | 145 |
    146 | 147 | 153 |
    154 |
    155 |
    156 | 157 | 160 | 161 |
    162 | ) 163 | } 164 | } 165 | 166 | const styles = StyleSheet.create({ 167 | palette: { 168 | display: 'flex', 169 | flexFlow: 'row', 170 | borderRadius: 3, 171 | overflow: 'hidden', 172 | }, 173 | numberInput: { 174 | margin: 0, 175 | padding: 10, 176 | fontSize: 13, 177 | '::-webkit-inner-spin-button': { 178 | opacity: 1, 179 | padding: 4, 180 | }, 181 | '::-webkit-outer-spin-button': { 182 | opacity: 1, 183 | padding: 4, 184 | }, 185 | }, 186 | scale: { 187 | display: 'flex', 188 | paddingTop: 18, 189 | marginTop: 40, 190 | marginBottom: -20, 191 | borderTop: '1px solid #EDEDED', 192 | }, 193 | range: { 194 | position: 'relative', 195 | flex: '1 0 0', 196 | borderRadius: 3, 197 | height: 20, 198 | }, 199 | resetButton: { 200 | marginRight: 15, 201 | marginTop: 0, 202 | }, 203 | }) 204 | 205 | export default ColorScaleView 206 | -------------------------------------------------------------------------------- /chromatic-sketch.sketchplugin/Contents/Resources/rc-slider.css: -------------------------------------------------------------------------------- 1 | .rc-slider { 2 | position: relative; 3 | height: 14px; 4 | padding: 5px 0; 5 | width: 100%; 6 | border-radius: 6px; 7 | -ms-touch-action: none; 8 | touch-action: none; 9 | box-sizing: border-box; 10 | -webkit-tap-highlight-color: rgba(0, 0, 0, 0); 11 | } 12 | .rc-slider * { 13 | box-sizing: border-box; 14 | -webkit-tap-highlight-color: rgba(0, 0, 0, 0); 15 | } 16 | .rc-slider-rail { 17 | position: absolute; 18 | width: 100%; 19 | background-color: #e9e9e9; 20 | height: 4px; 21 | border-radius: 6px; 22 | } 23 | .rc-slider-track { 24 | position: absolute; 25 | left: 0; 26 | height: 4px; 27 | border-radius: 6px; 28 | background-color: #abe2fb; 29 | } 30 | .rc-slider-handle { 31 | position: absolute; 32 | margin-left: -7px; 33 | margin-top: -5px; 34 | width: 14px; 35 | height: 14px; 36 | cursor: pointer; 37 | cursor: -webkit-grab; 38 | cursor: grab; 39 | border-radius: 50%; 40 | border: solid 2px #96dbfa; 41 | background-color: #fff; 42 | -ms-touch-action: pan-x; 43 | touch-action: pan-x; 44 | } 45 | .rc-slider-handle:hover { 46 | border-color: #57c5f7; 47 | } 48 | .rc-slider-handle:active { 49 | border-color: #57c5f7; 50 | box-shadow: 0 0 5px #57c5f7; 51 | cursor: -webkit-grabbing; 52 | cursor: grabbing; 53 | } 54 | .rc-slider-handle:focus { 55 | border-color: #57c5f7; 56 | box-shadow: 0 0 0 5px #96dbfa; 57 | outline: none; 58 | } 59 | .rc-slider-mark { 60 | position: absolute; 61 | top: 18px; 62 | left: 0; 63 | width: 100%; 64 | font-size: 12px; 65 | } 66 | .rc-slider-mark-text { 67 | position: absolute; 68 | display: inline-block; 69 | vertical-align: middle; 70 | text-align: center; 71 | cursor: pointer; 72 | color: #999; 73 | } 74 | .rc-slider-mark-text-active { 75 | color: #666; 76 | } 77 | .rc-slider-step { 78 | position: absolute; 79 | width: 100%; 80 | height: 4px; 81 | background: transparent; 82 | } 83 | .rc-slider-dot { 84 | position: absolute; 85 | bottom: -2px; 86 | margin-left: -4px; 87 | width: 8px; 88 | height: 8px; 89 | border: 2px solid #e9e9e9; 90 | background-color: #fff; 91 | cursor: pointer; 92 | border-radius: 50%; 93 | vertical-align: middle; 94 | } 95 | .rc-slider-dot:first-child { 96 | margin-left: -4px; 97 | } 98 | .rc-slider-dot:last-child { 99 | margin-left: -4px; 100 | } 101 | .rc-slider-dot-active { 102 | border-color: #96dbfa; 103 | } 104 | .rc-slider-disabled { 105 | background-color: #e9e9e9; 106 | } 107 | .rc-slider-disabled .rc-slider-track { 108 | background-color: #ccc; 109 | } 110 | .rc-slider-disabled .rc-slider-handle, 111 | .rc-slider-disabled .rc-slider-dot { 112 | border-color: #ccc; 113 | box-shadow: none; 114 | background-color: #fff; 115 | cursor: not-allowed; 116 | } 117 | .rc-slider-disabled .rc-slider-mark-text, 118 | .rc-slider-disabled .rc-slider-dot { 119 | cursor: not-allowed !important; 120 | } 121 | .rc-slider-vertical { 122 | width: 14px; 123 | height: 100%; 124 | padding: 0 5px; 125 | } 126 | .rc-slider-vertical .rc-slider-rail { 127 | height: 100%; 128 | width: 4px; 129 | } 130 | .rc-slider-vertical .rc-slider-track { 131 | left: 5px; 132 | bottom: 0; 133 | width: 4px; 134 | } 135 | .rc-slider-vertical .rc-slider-handle { 136 | margin-left: -5px; 137 | margin-bottom: -7px; 138 | -ms-touch-action: pan-y; 139 | touch-action: pan-y; 140 | } 141 | .rc-slider-vertical .rc-slider-mark { 142 | top: 0; 143 | left: 18px; 144 | height: 100%; 145 | } 146 | .rc-slider-vertical .rc-slider-step { 147 | height: 100%; 148 | width: 4px; 149 | } 150 | .rc-slider-vertical .rc-slider-dot { 151 | left: 2px; 152 | margin-bottom: -4px; 153 | } 154 | .rc-slider-vertical .rc-slider-dot:first-child { 155 | margin-bottom: -4px; 156 | } 157 | .rc-slider-vertical .rc-slider-dot:last-child { 158 | margin-bottom: -4px; 159 | } 160 | .rc-slider-tooltip-zoom-down-enter, 161 | .rc-slider-tooltip-zoom-down-appear { 162 | -webkit-animation-duration: .3s; 163 | animation-duration: .3s; 164 | -webkit-animation-fill-mode: both; 165 | animation-fill-mode: both; 166 | display: block !important; 167 | -webkit-animation-play-state: paused; 168 | animation-play-state: paused; 169 | } 170 | .rc-slider-tooltip-zoom-down-leave { 171 | -webkit-animation-duration: .3s; 172 | animation-duration: .3s; 173 | -webkit-animation-fill-mode: both; 174 | animation-fill-mode: both; 175 | display: block !important; 176 | -webkit-animation-play-state: paused; 177 | animation-play-state: paused; 178 | } 179 | .rc-slider-tooltip-zoom-down-enter.rc-slider-tooltip-zoom-down-enter-active, 180 | .rc-slider-tooltip-zoom-down-appear.rc-slider-tooltip-zoom-down-appear-active { 181 | -webkit-animation-name: rcSliderTooltipZoomDownIn; 182 | animation-name: rcSliderTooltipZoomDownIn; 183 | -webkit-animation-play-state: running; 184 | animation-play-state: running; 185 | } 186 | .rc-slider-tooltip-zoom-down-leave.rc-slider-tooltip-zoom-down-leave-active { 187 | -webkit-animation-name: rcSliderTooltipZoomDownOut; 188 | animation-name: rcSliderTooltipZoomDownOut; 189 | -webkit-animation-play-state: running; 190 | animation-play-state: running; 191 | } 192 | .rc-slider-tooltip-zoom-down-enter, 193 | .rc-slider-tooltip-zoom-down-appear { 194 | -webkit-transform: scale(0, 0); 195 | transform: scale(0, 0); 196 | -webkit-animation-timing-function: cubic-bezier(0.23, 1, 0.32, 1); 197 | animation-timing-function: cubic-bezier(0.23, 1, 0.32, 1); 198 | } 199 | .rc-slider-tooltip-zoom-down-leave { 200 | -webkit-animation-timing-function: cubic-bezier(0.755, 0.05, 0.855, 0.06); 201 | animation-timing-function: cubic-bezier(0.755, 0.05, 0.855, 0.06); 202 | } 203 | @-webkit-keyframes rcSliderTooltipZoomDownIn { 204 | 0% { 205 | opacity: 0; 206 | -webkit-transform-origin: 50% 100%; 207 | transform-origin: 50% 100%; 208 | -webkit-transform: scale(0, 0); 209 | transform: scale(0, 0); 210 | } 211 | 100% { 212 | -webkit-transform-origin: 50% 100%; 213 | transform-origin: 50% 100%; 214 | -webkit-transform: scale(1, 1); 215 | transform: scale(1, 1); 216 | } 217 | } 218 | @keyframes rcSliderTooltipZoomDownIn { 219 | 0% { 220 | opacity: 0; 221 | -webkit-transform-origin: 50% 100%; 222 | transform-origin: 50% 100%; 223 | -webkit-transform: scale(0, 0); 224 | transform: scale(0, 0); 225 | } 226 | 100% { 227 | -webkit-transform-origin: 50% 100%; 228 | transform-origin: 50% 100%; 229 | -webkit-transform: scale(1, 1); 230 | transform: scale(1, 1); 231 | } 232 | } 233 | @-webkit-keyframes rcSliderTooltipZoomDownOut { 234 | 0% { 235 | -webkit-transform-origin: 50% 100%; 236 | transform-origin: 50% 100%; 237 | -webkit-transform: scale(1, 1); 238 | transform: scale(1, 1); 239 | } 240 | 100% { 241 | opacity: 0; 242 | -webkit-transform-origin: 50% 100%; 243 | transform-origin: 50% 100%; 244 | -webkit-transform: scale(0, 0); 245 | transform: scale(0, 0); 246 | } 247 | } 248 | @keyframes rcSliderTooltipZoomDownOut { 249 | 0% { 250 | -webkit-transform-origin: 50% 100%; 251 | transform-origin: 50% 100%; 252 | -webkit-transform: scale(1, 1); 253 | transform: scale(1, 1); 254 | } 255 | 100% { 256 | opacity: 0; 257 | -webkit-transform-origin: 50% 100%; 258 | transform-origin: 50% 100%; 259 | -webkit-transform: scale(0, 0); 260 | transform: scale(0, 0); 261 | } 262 | } 263 | .rc-slider-tooltip { 264 | position: absolute; 265 | left: -9999px; 266 | top: -9999px; 267 | visibility: visible; 268 | box-sizing: border-box; 269 | -webkit-tap-highlight-color: rgba(0, 0, 0, 0); 270 | } 271 | .rc-slider-tooltip * { 272 | box-sizing: border-box; 273 | -webkit-tap-highlight-color: rgba(0, 0, 0, 0); 274 | } 275 | .rc-slider-tooltip-hidden { 276 | display: none; 277 | } 278 | .rc-slider-tooltip-placement-top { 279 | padding: 4px 0 8px 0; 280 | } 281 | .rc-slider-tooltip-inner { 282 | padding: 6px 2px; 283 | min-width: 24px; 284 | height: 24px; 285 | font-size: 12px; 286 | line-height: 1; 287 | color: #fff; 288 | text-align: center; 289 | text-decoration: none; 290 | background-color: #6c6c6c; 291 | border-radius: 6px; 292 | box-shadow: 0 0 4px #d9d9d9; 293 | } 294 | .rc-slider-tooltip-arrow { 295 | position: absolute; 296 | width: 0; 297 | height: 0; 298 | border-color: transparent; 299 | border-style: solid; 300 | } 301 | .rc-slider-tooltip-placement-top .rc-slider-tooltip-arrow { 302 | bottom: 4px; 303 | left: 50%; 304 | margin-left: -4px; 305 | border-width: 4px 4px 0; 306 | border-top-color: #6c6c6c; 307 | } 308 | 309 | /*# sourceMappingURL=rc-slider.css.map*/ -------------------------------------------------------------------------------- /chromatic-sketch.sketchplugin/Contents/Sketch/gradient.js: -------------------------------------------------------------------------------- 1 | var that = this; 2 | function run (key, context) { 3 | that.context = context; 4 | 5 | var exports = 6 | /******/ (function(modules) { // webpackBootstrap 7 | /******/ // The module cache 8 | /******/ var installedModules = {}; 9 | /******/ 10 | /******/ // The require function 11 | /******/ function __webpack_require__(moduleId) { 12 | /******/ 13 | /******/ // Check if module is in cache 14 | /******/ if(installedModules[moduleId]) { 15 | /******/ return installedModules[moduleId].exports; 16 | /******/ } 17 | /******/ // Create a new module (and put it into the cache) 18 | /******/ var module = installedModules[moduleId] = { 19 | /******/ i: moduleId, 20 | /******/ l: false, 21 | /******/ exports: {} 22 | /******/ }; 23 | /******/ 24 | /******/ // Execute the module function 25 | /******/ modules[moduleId].call(module.exports, module, module.exports, __webpack_require__); 26 | /******/ 27 | /******/ // Flag the module as loaded 28 | /******/ module.l = true; 29 | /******/ 30 | /******/ // Return the exports of the module 31 | /******/ return module.exports; 32 | /******/ } 33 | /******/ 34 | /******/ 35 | /******/ // expose the modules object (__webpack_modules__) 36 | /******/ __webpack_require__.m = modules; 37 | /******/ 38 | /******/ // expose the module cache 39 | /******/ __webpack_require__.c = installedModules; 40 | /******/ 41 | /******/ // define getter function for harmony exports 42 | /******/ __webpack_require__.d = function(exports, name, getter) { 43 | /******/ if(!__webpack_require__.o(exports, name)) { 44 | /******/ Object.defineProperty(exports, name, { 45 | /******/ configurable: false, 46 | /******/ enumerable: true, 47 | /******/ get: getter 48 | /******/ }); 49 | /******/ } 50 | /******/ }; 51 | /******/ 52 | /******/ // getDefaultExport function for compatibility with non-harmony modules 53 | /******/ __webpack_require__.n = function(module) { 54 | /******/ var getter = module && module.__esModule ? 55 | /******/ function getDefault() { return module['default']; } : 56 | /******/ function getModuleExports() { return module; }; 57 | /******/ __webpack_require__.d(getter, 'a', getter); 58 | /******/ return getter; 59 | /******/ }; 60 | /******/ 61 | /******/ // Object.prototype.hasOwnProperty.call 62 | /******/ __webpack_require__.o = function(object, property) { return Object.prototype.hasOwnProperty.call(object, property); }; 63 | /******/ 64 | /******/ // __webpack_public_path__ 65 | /******/ __webpack_require__.p = ""; 66 | /******/ 67 | /******/ // Load entry module and return exports 68 | /******/ return __webpack_require__(__webpack_require__.s = 0); 69 | /******/ }) 70 | /************************************************************************/ 71 | /******/ ([ 72 | /* 0 */ 73 | /***/ (function(module, exports, __webpack_require__) { 74 | 75 | Object.defineProperty(exports, "__esModule", { 76 | value: true 77 | }); 78 | 79 | exports['default'] = function (context) { 80 | if (context.selection.length == 0) { 81 | (0, _helpers.buildDialog)('Fix Gradient', 'Select a shape with a gradient first').runModal(); 82 | return; 83 | } 84 | 85 | var selected = context.selection[0]; 86 | var gradient = selected.style().fills().firstObject().gradient(); 87 | var positionArray = []; 88 | var colorArray = []; 89 | 90 | for (var i = 0; i < gradient.stops().length; i++) { 91 | var stop = gradient.stops()[i]; 92 | colorArray.push(String(stop.color().immutableModelObject().stringValueWithAlpha(true))); 93 | positionArray.push(stop.position()); 94 | } 95 | 96 | var handlers = { 97 | ready: function () { 98 | function ready() { 99 | webview.eval('window.renderGradientView(' + String(JSON.stringify(colorArray)) + ', ' + String(JSON.stringify(positionArray)) + ')'); 100 | } 101 | 102 | return ready; 103 | }(), 104 | applyGradient: function () { 105 | function applyGradient(stopArray) { 106 | var sketchStopArray = []; 107 | stopArray.forEach(function (stop) { 108 | sketchStopArray.push(makeStop(stop.position, stop.color)); 109 | }); 110 | gradient.setStops(sketchStopArray); 111 | webview.close(); 112 | } 113 | 114 | return applyGradient; 115 | }() 116 | }; 117 | 118 | var webview = (0, _helpers.createWebview)(context, handlers, 'Fix Gradient', 410); 119 | }; 120 | 121 | var _helpers = __webpack_require__(1); 122 | 123 | function makeStop(position, color) { 124 | return MSGradientStop.stopWithPosition_color_(position, (0, _helpers.makeColor)(color)); 125 | } 126 | 127 | /***/ }), 128 | /* 1 */ 129 | /***/ (function(module, exports, __webpack_require__) { 130 | 131 | Object.defineProperty(exports, "__esModule", { 132 | value: true 133 | }); 134 | exports.makeColor = makeColor; 135 | exports.buildDialog = buildDialog; 136 | exports.createWebview = createWebview; 137 | 138 | var _sketchModuleWebView = __webpack_require__(2); 139 | 140 | var _sketchModuleWebView2 = _interopRequireDefault(_sketchModuleWebView); 141 | 142 | function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { 'default': obj }; } 143 | 144 | function makeColor(c) { 145 | return MSImmutableColor.colorWithRed_green_blue_alpha(c[0] / 255, c[1] / 255, c[2] / 255, c[3]).newMutableCounterpart(); 146 | } 147 | 148 | function buildDialog(message, informativeText) { 149 | var alert = COSAlertWindow['new'](); 150 | alert.setMessageText(message); 151 | alert.setInformativeText(informativeText); 152 | return alert; 153 | } 154 | 155 | function createWebview(context, handlers, title, height) { 156 | var v = 242 / 255; 157 | var grayColor = NSColor.colorWithRed_green_blue_alpha(v, v, v, 1); 158 | var options = { 159 | identifier: 'unique.id', 160 | x: 0, 161 | y: 0, 162 | width: 630, 163 | height: height, 164 | background: grayColor, 165 | blurredBackground: false, 166 | onlyShowCloseButton: false, 167 | title: title, 168 | hideTitleBar: false, 169 | shouldKeepAround: true, 170 | resizable: false, 171 | handlers: handlers 172 | }; 173 | return new _sketchModuleWebView2['default'](context, 'index.html', options); 174 | } 175 | 176 | /***/ }), 177 | /* 2 */ 178 | /***/ (function(module, exports, __webpack_require__) { 179 | 180 | /* globals NSUUID NSThread NSPanel NSMakeRect NSTexturedBackgroundWindowMask NSTitledWindowMask NSWindowTitleHidden NSClosableWindowMask NSColor NSWindowMiniaturizeButton NSWindowZoomButton NSFloatingWindowLevel WebView COScript NSWindowCloseButton NSFullSizeContentViewWindowMask NSVisualEffectView NSAppearance NSAppearanceNameVibrantLight NSVisualEffectBlendingModeBehindWindow NSLayoutConstraint NSLayoutRelationEqual NSLayoutAttributeLeft NSLayoutAttributeTop NSLayoutAttributeRight NSLayoutAttributeBottom NSResizableWindowMask */ 181 | var MochaJSDelegate = __webpack_require__(3) 182 | var parseQuery = __webpack_require__(4) 183 | 184 | var coScript = COScript.currentCOScript() 185 | 186 | var LOCATION_CHANGED = 'webView:didChangeLocationWithinPageForFrame:' 187 | 188 | function addEdgeConstraint (edge, subview, view, constant) { 189 | view.addConstraint(NSLayoutConstraint.constraintWithItem_attribute_relatedBy_toItem_attribute_multiplier_constant( 190 | subview, 191 | edge, 192 | NSLayoutRelationEqual, 193 | view, 194 | edge, 195 | 1, 196 | constant 197 | )) 198 | } 199 | function fitSubviewToView (subview, view, constants) { 200 | subview.setTranslatesAutoresizingMaskIntoConstraints(false) 201 | 202 | addEdgeConstraint(NSLayoutAttributeLeft, subview, view, constants[0]) 203 | addEdgeConstraint(NSLayoutAttributeTop, subview, view, constants[1]) 204 | addEdgeConstraint(NSLayoutAttributeRight, subview, view, constants[2]) 205 | addEdgeConstraint(NSLayoutAttributeBottom, subview, view, constants[3]) 206 | } 207 | 208 | function WebUI (context, frameLocation, options) { 209 | options = options || {} 210 | var identifier = options.identifier || NSUUID.UUID().UUIDString() 211 | var threadDictionary = NSThread.mainThread().threadDictionary() 212 | 213 | var panel 214 | var webView 215 | 216 | // if we already have a panel opened, reuse it 217 | if (threadDictionary[identifier]) { 218 | panel = threadDictionary[identifier] 219 | panel.makeKeyAndOrderFront(null) 220 | 221 | var subviews = panel.contentView().subviews() 222 | for (var i = 0; i < subviews.length; i++) { 223 | if (subviews[i].isKindOfClass(WebView.class())) { 224 | webView = subviews[i] 225 | } 226 | } 227 | 228 | if (!webView) { 229 | throw new Error('Tried to reuse panel but couldn\'t find the webview inside') 230 | } 231 | 232 | return { 233 | panel: panel, 234 | eval: webView.stringByEvaluatingJavaScriptFromString, 235 | webView: webView 236 | } 237 | } 238 | 239 | panel = NSPanel.alloc().init() 240 | 241 | // Window size 242 | var panelWidth = options.width || 240 243 | var panelHeight = options.height || 180 244 | panel.setFrame_display(NSMakeRect( 245 | options.x || 0, 246 | options.y || 0, 247 | panelWidth, 248 | panelHeight 249 | ), true) 250 | 251 | // Titlebar 252 | panel.setTitle(options.title || context.plugin.name()) 253 | if (options.hideTitleBar) { 254 | panel.setTitlebarAppearsTransparent(true) 255 | panel.setTitleVisibility(NSWindowTitleHidden) 256 | } 257 | 258 | // Hide minize and zoom buttons 259 | if (options.onlyShowCloseButton) { 260 | panel.standardWindowButton(NSWindowMiniaturizeButton).setHidden(true) 261 | panel.standardWindowButton(NSWindowZoomButton).setHidden(true) 262 | } 263 | 264 | // Close window callback 265 | var closeButton = panel.standardWindowButton(NSWindowCloseButton) 266 | function closeHandler () { 267 | if (options.onPanelClose) { 268 | var result = options.onPanelClose() 269 | if (result === false) { 270 | return 271 | } 272 | } 273 | panel.close() 274 | threadDictionary.removeObjectForKey(options.identifier) 275 | coScript.setShouldKeepAround(false) 276 | } 277 | 278 | closeButton.setCOSJSTargetFunction(closeHandler) 279 | closeButton.setAction('callAction:') 280 | 281 | panel.setStyleMask(options.styleMask || ( 282 | options.resizable 283 | ? (NSTexturedBackgroundWindowMask | NSTitledWindowMask | NSResizableWindowMask | NSClosableWindowMask | NSFullSizeContentViewWindowMask) 284 | : (NSTexturedBackgroundWindowMask | NSTitledWindowMask | NSClosableWindowMask | NSFullSizeContentViewWindowMask) 285 | )) 286 | panel.becomeKeyWindow() 287 | panel.setLevel(NSFloatingWindowLevel) 288 | 289 | // Appearance 290 | var backgroundColor = options.background || NSColor.whiteColor() 291 | panel.setBackgroundColor(backgroundColor) 292 | if (options.blurredBackground) { 293 | var vibrancy = NSVisualEffectView.alloc().initWithFrame(NSMakeRect(0, 0, panelWidth, panelHeight)) 294 | vibrancy.setAppearance(NSAppearance.appearanceNamed(NSAppearanceNameVibrantLight)) 295 | vibrancy.setBlendingMode(NSVisualEffectBlendingModeBehindWindow) 296 | 297 | // Add it to the panel 298 | panel.contentView().addSubview(vibrancy) 299 | fitSubviewToView(vibrancy, panel.contentView(), [0, 0, 0, 0]) 300 | } 301 | 302 | threadDictionary[identifier] = panel 303 | 304 | if (options.shouldKeepAround !== false) { // Long-running script 305 | coScript.setShouldKeepAround(true) 306 | } 307 | 308 | // Add Web View to window 309 | webView = WebView.alloc().initWithFrame(NSMakeRect( 310 | 0, 311 | options.hideTitleBar ? -24 : 0, 312 | options.width || 240, 313 | (options.height || 180) - (options.hideTitleBar ? 0 : 24) 314 | )) 315 | 316 | if (options.frameLoadDelegate || options.handlers) { 317 | var handlers = options.frameLoadDelegate || {} 318 | if (options.handlers) { 319 | var lastQueryId 320 | handlers[LOCATION_CHANGED] = function (webview, frame) { 321 | var query = webview.windowScriptObject().evaluateWebScript('window.location.hash') 322 | query = parseQuery(query) 323 | if (query.pluginAction && query.actionId && query.actionId !== lastQueryId && query.pluginAction in options.handlers) { 324 | lastQueryId = query.actionId 325 | try { 326 | query.pluginArgs = JSON.parse(query.pluginArgs) 327 | } catch (err) {} 328 | options.handlers[query.pluginAction].apply(context, query.pluginArgs) 329 | } 330 | } 331 | } 332 | var frameLoadDelegate = new MochaJSDelegate(handlers) 333 | webView.setFrameLoadDelegate_(frameLoadDelegate.getClassInstance()) 334 | } 335 | if (options.uiDelegate) { 336 | var uiDelegate = new MochaJSDelegate(options.uiDelegate) 337 | webView.setUIDelegate_(uiDelegate.getClassInstance()) 338 | } 339 | 340 | if (!options.blurredBackground) { 341 | webView.setOpaque(true) 342 | webView.setBackgroundColor(backgroundColor) 343 | } else { 344 | // Prevent it from drawing a white background 345 | webView.setDrawsBackground(false) 346 | } 347 | 348 | // When frameLocation is a file, prefix it with the Sketch Resources path 349 | if ((/^(?!http|localhost|www|file).*\.html?$/).test(frameLocation)) { 350 | frameLocation = context.plugin.urlForResourceNamed(frameLocation).path() 351 | } 352 | webView.setMainFrameURL_(frameLocation) 353 | 354 | panel.contentView().addSubview(webView) 355 | fitSubviewToView(webView, panel.contentView(), [ 356 | 0, options.hideTitleBar ? 0 : 24, 0, 0 357 | ]) 358 | 359 | panel.center() 360 | panel.makeKeyAndOrderFront(null) 361 | 362 | return { 363 | panel: panel, 364 | eval: webView.stringByEvaluatingJavaScriptFromString, 365 | webView: webView, 366 | close: closeHandler 367 | } 368 | } 369 | 370 | WebUI.clean = function () { 371 | coScript.setShouldKeepAround(false) 372 | } 373 | 374 | module.exports = WebUI 375 | 376 | 377 | /***/ }), 378 | /* 3 */ 379 | /***/ (function(module, exports) { 380 | 381 | /* globals NSUUID MOClassDescription NSObject NSSelectorFromString NSClassFromString */ 382 | 383 | module.exports = function (selectorHandlerDict, superclass) { 384 | var uniqueClassName = 'MochaJSDelegate_DynamicClass_' + NSUUID.UUID().UUIDString() 385 | 386 | var delegateClassDesc = MOClassDescription.allocateDescriptionForClassWithName_superclass_(uniqueClassName, superclass || NSObject) 387 | 388 | delegateClassDesc.registerClass() 389 | 390 | // Storage Handlers 391 | var handlers = {} 392 | 393 | // Define interface 394 | this.setHandlerForSelector = function (selectorString, func) { 395 | var handlerHasBeenSet = (selectorString in handlers) 396 | var selector = NSSelectorFromString(selectorString) 397 | 398 | handlers[selectorString] = func 399 | 400 | /* 401 | For some reason, Mocha acts weird about arguments: https://github.com/logancollins/Mocha/issues/28 402 | We have to basically create a dynamic handler with a likewise dynamic number of predefined arguments. 403 | */ 404 | if (!handlerHasBeenSet) { 405 | var args = [] 406 | var regex = /:/g 407 | while (regex.exec(selectorString)) { 408 | args.push('arg' + args.length) 409 | } 410 | 411 | var dynamicFunction = eval('(function (' + args.join(', ') + ') { return handlers[selectorString].apply(this, arguments); })') 412 | 413 | delegateClassDesc.addInstanceMethodWithSelector_function_(selector, dynamicFunction) 414 | } 415 | } 416 | 417 | this.removeHandlerForSelector = function (selectorString) { 418 | delete handlers[selectorString] 419 | } 420 | 421 | this.getHandlerForSelector = function (selectorString) { 422 | return handlers[selectorString] 423 | } 424 | 425 | this.getAllHandlers = function () { 426 | return handlers 427 | } 428 | 429 | this.getClass = function () { 430 | return NSClassFromString(uniqueClassName) 431 | } 432 | 433 | this.getClassInstance = function () { 434 | return NSClassFromString(uniqueClassName).new() 435 | } 436 | 437 | // Convenience 438 | if (typeof selectorHandlerDict === 'object') { 439 | for (var selectorString in selectorHandlerDict) { 440 | this.setHandlerForSelector(selectorString, selectorHandlerDict[selectorString]) 441 | } 442 | } 443 | } 444 | 445 | 446 | /***/ }), 447 | /* 4 */ 448 | /***/ (function(module, exports) { 449 | 450 | module.exports = function (query) { 451 | query = query.split('?')[1] 452 | if (!query) { return } 453 | query = query.split('&').reduce(function (prev, s) { 454 | var res = s.split('=') 455 | if (res.length === 2) { 456 | prev[decodeURIComponent(res[0])] = decodeURIComponent(res[1]) 457 | } 458 | return prev 459 | }, {}) 460 | return query 461 | } 462 | 463 | 464 | /***/ }) 465 | /******/ ]); 466 | if (key === 'default' && typeof exports === 'function') { 467 | exports(context); 468 | } else { 469 | exports[key](context); 470 | } 471 | } 472 | that['onRun'] = run.bind(this, 'default') 473 | -------------------------------------------------------------------------------- /chromatic-sketch.sketchplugin/Contents/Sketch/color-scale.js: -------------------------------------------------------------------------------- 1 | var that = this; 2 | function run (key, context) { 3 | that.context = context; 4 | 5 | var exports = 6 | /******/ (function(modules) { // webpackBootstrap 7 | /******/ // The module cache 8 | /******/ var installedModules = {}; 9 | /******/ 10 | /******/ // The require function 11 | /******/ function __webpack_require__(moduleId) { 12 | /******/ 13 | /******/ // Check if module is in cache 14 | /******/ if(installedModules[moduleId]) { 15 | /******/ return installedModules[moduleId].exports; 16 | /******/ } 17 | /******/ // Create a new module (and put it into the cache) 18 | /******/ var module = installedModules[moduleId] = { 19 | /******/ i: moduleId, 20 | /******/ l: false, 21 | /******/ exports: {} 22 | /******/ }; 23 | /******/ 24 | /******/ // Execute the module function 25 | /******/ modules[moduleId].call(module.exports, module, module.exports, __webpack_require__); 26 | /******/ 27 | /******/ // Flag the module as loaded 28 | /******/ module.l = true; 29 | /******/ 30 | /******/ // Return the exports of the module 31 | /******/ return module.exports; 32 | /******/ } 33 | /******/ 34 | /******/ 35 | /******/ // expose the modules object (__webpack_modules__) 36 | /******/ __webpack_require__.m = modules; 37 | /******/ 38 | /******/ // expose the module cache 39 | /******/ __webpack_require__.c = installedModules; 40 | /******/ 41 | /******/ // define getter function for harmony exports 42 | /******/ __webpack_require__.d = function(exports, name, getter) { 43 | /******/ if(!__webpack_require__.o(exports, name)) { 44 | /******/ Object.defineProperty(exports, name, { 45 | /******/ configurable: false, 46 | /******/ enumerable: true, 47 | /******/ get: getter 48 | /******/ }); 49 | /******/ } 50 | /******/ }; 51 | /******/ 52 | /******/ // getDefaultExport function for compatibility with non-harmony modules 53 | /******/ __webpack_require__.n = function(module) { 54 | /******/ var getter = module && module.__esModule ? 55 | /******/ function getDefault() { return module['default']; } : 56 | /******/ function getModuleExports() { return module; }; 57 | /******/ __webpack_require__.d(getter, 'a', getter); 58 | /******/ return getter; 59 | /******/ }; 60 | /******/ 61 | /******/ // Object.prototype.hasOwnProperty.call 62 | /******/ __webpack_require__.o = function(object, property) { return Object.prototype.hasOwnProperty.call(object, property); }; 63 | /******/ 64 | /******/ // __webpack_public_path__ 65 | /******/ __webpack_require__.p = ""; 66 | /******/ 67 | /******/ // Load entry module and return exports 68 | /******/ return __webpack_require__(__webpack_require__.s = 0); 69 | /******/ }) 70 | /************************************************************************/ 71 | /******/ ([ 72 | /* 0 */ 73 | /***/ (function(module, exports, __webpack_require__) { 74 | 75 | Object.defineProperty(exports, "__esModule", { 76 | value: true 77 | }); 78 | 79 | exports['default'] = function (context) { 80 | var sketch = context.api(); 81 | var document = sketch.selectedDocument; 82 | var selection = document.selectedLayers; 83 | var page = document.selectedPage; 84 | var layerColors = []; 85 | 86 | selection.iterate(function (layer) { 87 | if (layer.isShape) { 88 | layerColors.push(String(layer.style.sketchObject.fills().firstObject().color().immutableModelObject().stringValueWithAlpha(true))); 89 | } 90 | }); 91 | 92 | var handlers = { 93 | ready: function () { 94 | function ready() { 95 | webview.eval('window.renderColorScaleView(\'' + String(layerColors[0] || '#dddddd') + '\', \'' + String(layerColors[1] || '#000000') + '\')'); 96 | } 97 | 98 | return ready; 99 | }(), 100 | insert: function () { 101 | function insert(colorArray) { 102 | webview.close(); 103 | var group = page.newGroup({ 104 | frame: new sketch.Rectangle(0, 0, PALETTE_WIDTH, PALETTE_HEIGHT), 105 | name: 'Color Scale' 106 | }); 107 | var swatchWidth = PALETTE_WIDTH / colorArray.length; 108 | 109 | for (var i = 0; i < colorArray.length; i++) { 110 | var myStyle = new sketch.Style(); 111 | myStyle.borders = ''; 112 | myStyle.sketchObject.fills().firstObject().setColor((0, _helpers.makeColor)(colorArray[i])); 113 | var rect = group.newShape({ 114 | frame: new sketch.Rectangle(swatchWidth * i, 0, swatchWidth, 100), 115 | style: myStyle 116 | }); 117 | } 118 | } 119 | 120 | return insert; 121 | }() 122 | }; 123 | 124 | var webview = (0, _helpers.createWebview)(context, handlers, 'Create Color Scale', 435); 125 | }; 126 | 127 | var _helpers = __webpack_require__(1); 128 | 129 | var PALETTE_WIDTH = 550; 130 | var PALETTE_HEIGHT = 100; 131 | 132 | /***/ }), 133 | /* 1 */ 134 | /***/ (function(module, exports, __webpack_require__) { 135 | 136 | Object.defineProperty(exports, "__esModule", { 137 | value: true 138 | }); 139 | exports.makeColor = makeColor; 140 | exports.buildDialog = buildDialog; 141 | exports.createWebview = createWebview; 142 | 143 | var _sketchModuleWebView = __webpack_require__(2); 144 | 145 | var _sketchModuleWebView2 = _interopRequireDefault(_sketchModuleWebView); 146 | 147 | function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { 'default': obj }; } 148 | 149 | function makeColor(c) { 150 | return MSImmutableColor.colorWithRed_green_blue_alpha(c[0] / 255, c[1] / 255, c[2] / 255, c[3]).newMutableCounterpart(); 151 | } 152 | 153 | function buildDialog(message, informativeText) { 154 | var alert = COSAlertWindow['new'](); 155 | alert.setMessageText(message); 156 | alert.setInformativeText(informativeText); 157 | return alert; 158 | } 159 | 160 | function createWebview(context, handlers, title, height) { 161 | var v = 242 / 255; 162 | var grayColor = NSColor.colorWithRed_green_blue_alpha(v, v, v, 1); 163 | var options = { 164 | identifier: 'unique.id', 165 | x: 0, 166 | y: 0, 167 | width: 630, 168 | height: height, 169 | background: grayColor, 170 | blurredBackground: false, 171 | onlyShowCloseButton: false, 172 | title: title, 173 | hideTitleBar: false, 174 | shouldKeepAround: true, 175 | resizable: false, 176 | handlers: handlers 177 | }; 178 | return new _sketchModuleWebView2['default'](context, 'index.html', options); 179 | } 180 | 181 | /***/ }), 182 | /* 2 */ 183 | /***/ (function(module, exports, __webpack_require__) { 184 | 185 | /* globals NSUUID NSThread NSPanel NSMakeRect NSTexturedBackgroundWindowMask NSTitledWindowMask NSWindowTitleHidden NSClosableWindowMask NSColor NSWindowMiniaturizeButton NSWindowZoomButton NSFloatingWindowLevel WebView COScript NSWindowCloseButton NSFullSizeContentViewWindowMask NSVisualEffectView NSAppearance NSAppearanceNameVibrantLight NSVisualEffectBlendingModeBehindWindow NSLayoutConstraint NSLayoutRelationEqual NSLayoutAttributeLeft NSLayoutAttributeTop NSLayoutAttributeRight NSLayoutAttributeBottom NSResizableWindowMask */ 186 | var MochaJSDelegate = __webpack_require__(3) 187 | var parseQuery = __webpack_require__(4) 188 | 189 | var coScript = COScript.currentCOScript() 190 | 191 | var LOCATION_CHANGED = 'webView:didChangeLocationWithinPageForFrame:' 192 | 193 | function addEdgeConstraint (edge, subview, view, constant) { 194 | view.addConstraint(NSLayoutConstraint.constraintWithItem_attribute_relatedBy_toItem_attribute_multiplier_constant( 195 | subview, 196 | edge, 197 | NSLayoutRelationEqual, 198 | view, 199 | edge, 200 | 1, 201 | constant 202 | )) 203 | } 204 | function fitSubviewToView (subview, view, constants) { 205 | subview.setTranslatesAutoresizingMaskIntoConstraints(false) 206 | 207 | addEdgeConstraint(NSLayoutAttributeLeft, subview, view, constants[0]) 208 | addEdgeConstraint(NSLayoutAttributeTop, subview, view, constants[1]) 209 | addEdgeConstraint(NSLayoutAttributeRight, subview, view, constants[2]) 210 | addEdgeConstraint(NSLayoutAttributeBottom, subview, view, constants[3]) 211 | } 212 | 213 | function WebUI (context, frameLocation, options) { 214 | options = options || {} 215 | var identifier = options.identifier || NSUUID.UUID().UUIDString() 216 | var threadDictionary = NSThread.mainThread().threadDictionary() 217 | 218 | var panel 219 | var webView 220 | 221 | // if we already have a panel opened, reuse it 222 | if (threadDictionary[identifier]) { 223 | panel = threadDictionary[identifier] 224 | panel.makeKeyAndOrderFront(null) 225 | 226 | var subviews = panel.contentView().subviews() 227 | for (var i = 0; i < subviews.length; i++) { 228 | if (subviews[i].isKindOfClass(WebView.class())) { 229 | webView = subviews[i] 230 | } 231 | } 232 | 233 | if (!webView) { 234 | throw new Error('Tried to reuse panel but couldn\'t find the webview inside') 235 | } 236 | 237 | return { 238 | panel: panel, 239 | eval: webView.stringByEvaluatingJavaScriptFromString, 240 | webView: webView 241 | } 242 | } 243 | 244 | panel = NSPanel.alloc().init() 245 | 246 | // Window size 247 | var panelWidth = options.width || 240 248 | var panelHeight = options.height || 180 249 | panel.setFrame_display(NSMakeRect( 250 | options.x || 0, 251 | options.y || 0, 252 | panelWidth, 253 | panelHeight 254 | ), true) 255 | 256 | // Titlebar 257 | panel.setTitle(options.title || context.plugin.name()) 258 | if (options.hideTitleBar) { 259 | panel.setTitlebarAppearsTransparent(true) 260 | panel.setTitleVisibility(NSWindowTitleHidden) 261 | } 262 | 263 | // Hide minize and zoom buttons 264 | if (options.onlyShowCloseButton) { 265 | panel.standardWindowButton(NSWindowMiniaturizeButton).setHidden(true) 266 | panel.standardWindowButton(NSWindowZoomButton).setHidden(true) 267 | } 268 | 269 | // Close window callback 270 | var closeButton = panel.standardWindowButton(NSWindowCloseButton) 271 | function closeHandler () { 272 | if (options.onPanelClose) { 273 | var result = options.onPanelClose() 274 | if (result === false) { 275 | return 276 | } 277 | } 278 | panel.close() 279 | threadDictionary.removeObjectForKey(options.identifier) 280 | coScript.setShouldKeepAround(false) 281 | } 282 | 283 | closeButton.setCOSJSTargetFunction(closeHandler) 284 | closeButton.setAction('callAction:') 285 | 286 | panel.setStyleMask(options.styleMask || ( 287 | options.resizable 288 | ? (NSTexturedBackgroundWindowMask | NSTitledWindowMask | NSResizableWindowMask | NSClosableWindowMask | NSFullSizeContentViewWindowMask) 289 | : (NSTexturedBackgroundWindowMask | NSTitledWindowMask | NSClosableWindowMask | NSFullSizeContentViewWindowMask) 290 | )) 291 | panel.becomeKeyWindow() 292 | panel.setLevel(NSFloatingWindowLevel) 293 | 294 | // Appearance 295 | var backgroundColor = options.background || NSColor.whiteColor() 296 | panel.setBackgroundColor(backgroundColor) 297 | if (options.blurredBackground) { 298 | var vibrancy = NSVisualEffectView.alloc().initWithFrame(NSMakeRect(0, 0, panelWidth, panelHeight)) 299 | vibrancy.setAppearance(NSAppearance.appearanceNamed(NSAppearanceNameVibrantLight)) 300 | vibrancy.setBlendingMode(NSVisualEffectBlendingModeBehindWindow) 301 | 302 | // Add it to the panel 303 | panel.contentView().addSubview(vibrancy) 304 | fitSubviewToView(vibrancy, panel.contentView(), [0, 0, 0, 0]) 305 | } 306 | 307 | threadDictionary[identifier] = panel 308 | 309 | if (options.shouldKeepAround !== false) { // Long-running script 310 | coScript.setShouldKeepAround(true) 311 | } 312 | 313 | // Add Web View to window 314 | webView = WebView.alloc().initWithFrame(NSMakeRect( 315 | 0, 316 | options.hideTitleBar ? -24 : 0, 317 | options.width || 240, 318 | (options.height || 180) - (options.hideTitleBar ? 0 : 24) 319 | )) 320 | 321 | if (options.frameLoadDelegate || options.handlers) { 322 | var handlers = options.frameLoadDelegate || {} 323 | if (options.handlers) { 324 | var lastQueryId 325 | handlers[LOCATION_CHANGED] = function (webview, frame) { 326 | var query = webview.windowScriptObject().evaluateWebScript('window.location.hash') 327 | query = parseQuery(query) 328 | if (query.pluginAction && query.actionId && query.actionId !== lastQueryId && query.pluginAction in options.handlers) { 329 | lastQueryId = query.actionId 330 | try { 331 | query.pluginArgs = JSON.parse(query.pluginArgs) 332 | } catch (err) {} 333 | options.handlers[query.pluginAction].apply(context, query.pluginArgs) 334 | } 335 | } 336 | } 337 | var frameLoadDelegate = new MochaJSDelegate(handlers) 338 | webView.setFrameLoadDelegate_(frameLoadDelegate.getClassInstance()) 339 | } 340 | if (options.uiDelegate) { 341 | var uiDelegate = new MochaJSDelegate(options.uiDelegate) 342 | webView.setUIDelegate_(uiDelegate.getClassInstance()) 343 | } 344 | 345 | if (!options.blurredBackground) { 346 | webView.setOpaque(true) 347 | webView.setBackgroundColor(backgroundColor) 348 | } else { 349 | // Prevent it from drawing a white background 350 | webView.setDrawsBackground(false) 351 | } 352 | 353 | // When frameLocation is a file, prefix it with the Sketch Resources path 354 | if ((/^(?!http|localhost|www|file).*\.html?$/).test(frameLocation)) { 355 | frameLocation = context.plugin.urlForResourceNamed(frameLocation).path() 356 | } 357 | webView.setMainFrameURL_(frameLocation) 358 | 359 | panel.contentView().addSubview(webView) 360 | fitSubviewToView(webView, panel.contentView(), [ 361 | 0, options.hideTitleBar ? 0 : 24, 0, 0 362 | ]) 363 | 364 | panel.center() 365 | panel.makeKeyAndOrderFront(null) 366 | 367 | return { 368 | panel: panel, 369 | eval: webView.stringByEvaluatingJavaScriptFromString, 370 | webView: webView, 371 | close: closeHandler 372 | } 373 | } 374 | 375 | WebUI.clean = function () { 376 | coScript.setShouldKeepAround(false) 377 | } 378 | 379 | module.exports = WebUI 380 | 381 | 382 | /***/ }), 383 | /* 3 */ 384 | /***/ (function(module, exports) { 385 | 386 | /* globals NSUUID MOClassDescription NSObject NSSelectorFromString NSClassFromString */ 387 | 388 | module.exports = function (selectorHandlerDict, superclass) { 389 | var uniqueClassName = 'MochaJSDelegate_DynamicClass_' + NSUUID.UUID().UUIDString() 390 | 391 | var delegateClassDesc = MOClassDescription.allocateDescriptionForClassWithName_superclass_(uniqueClassName, superclass || NSObject) 392 | 393 | delegateClassDesc.registerClass() 394 | 395 | // Storage Handlers 396 | var handlers = {} 397 | 398 | // Define interface 399 | this.setHandlerForSelector = function (selectorString, func) { 400 | var handlerHasBeenSet = (selectorString in handlers) 401 | var selector = NSSelectorFromString(selectorString) 402 | 403 | handlers[selectorString] = func 404 | 405 | /* 406 | For some reason, Mocha acts weird about arguments: https://github.com/logancollins/Mocha/issues/28 407 | We have to basically create a dynamic handler with a likewise dynamic number of predefined arguments. 408 | */ 409 | if (!handlerHasBeenSet) { 410 | var args = [] 411 | var regex = /:/g 412 | while (regex.exec(selectorString)) { 413 | args.push('arg' + args.length) 414 | } 415 | 416 | var dynamicFunction = eval('(function (' + args.join(', ') + ') { return handlers[selectorString].apply(this, arguments); })') 417 | 418 | delegateClassDesc.addInstanceMethodWithSelector_function_(selector, dynamicFunction) 419 | } 420 | } 421 | 422 | this.removeHandlerForSelector = function (selectorString) { 423 | delete handlers[selectorString] 424 | } 425 | 426 | this.getHandlerForSelector = function (selectorString) { 427 | return handlers[selectorString] 428 | } 429 | 430 | this.getAllHandlers = function () { 431 | return handlers 432 | } 433 | 434 | this.getClass = function () { 435 | return NSClassFromString(uniqueClassName) 436 | } 437 | 438 | this.getClassInstance = function () { 439 | return NSClassFromString(uniqueClassName).new() 440 | } 441 | 442 | // Convenience 443 | if (typeof selectorHandlerDict === 'object') { 444 | for (var selectorString in selectorHandlerDict) { 445 | this.setHandlerForSelector(selectorString, selectorHandlerDict[selectorString]) 446 | } 447 | } 448 | } 449 | 450 | 451 | /***/ }), 452 | /* 4 */ 453 | /***/ (function(module, exports) { 454 | 455 | module.exports = function (query) { 456 | query = query.split('?')[1] 457 | if (!query) { return } 458 | query = query.split('&').reduce(function (prev, s) { 459 | var res = s.split('=') 460 | if (res.length === 2) { 461 | prev[decodeURIComponent(res[0])] = decodeURIComponent(res[1]) 462 | } 463 | return prev 464 | }, {}) 465 | return query 466 | } 467 | 468 | 469 | /***/ }) 470 | /******/ ]); 471 | if (key === 'default' && typeof exports === 'function') { 472 | exports(context); 473 | } else { 474 | exports[key](context); 475 | } 476 | } 477 | that['onRun'] = run.bind(this, 'default') 478 | --------------------------------------------------------------------------------