├── .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 |
9 | Color Mode:
10 |
15 | Lab
16 | Lch
17 | HSL
18 | RGB
19 |
20 |
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 | 
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 | 
22 |
23 | #### Chromatic Sketch -> Create Color Scale
24 | This command will create a scale between the fill colors of two selected shapes.
25 |
26 | 
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 |
86 | Apply
87 |
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 |
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 |
143 | Reset
144 |
145 |
146 |
147 |
153 |
154 |
155 |
156 |
157 |
158 | Insert
159 |
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 |
--------------------------------------------------------------------------------