├── .babelrc ├── .eslintrc ├── .gitignore ├── .npmignore ├── .prettierignore ├── .prettierrc.yml ├── .storybook ├── .babelrc ├── addons.js ├── config.js └── webpack.config.js ├── .travis.yml ├── LICENSE ├── README.md ├── images ├── artboard.jpg ├── colors.jpg └── guides-demo.gif ├── jest.config.js ├── package.json ├── scripts ├── check-npm.js └── setupTests.js ├── src ├── @types │ ├── helpscout__fancy │ │ └── index.d.ts │ └── helpscout__react-utils │ │ └── index.d.ts ├── Artboard │ ├── Artboard.ActionTypes.ts │ ├── Artboard.Container.tsx │ ├── Artboard.KeyboardHints.tsx │ ├── Artboard.actions.ts │ ├── Artboard.css.ts │ ├── Artboard.reducers.ts │ ├── Artboard.store.ts │ ├── Artboard.tsx │ ├── Artboard.types.ts │ ├── Artboard.utils.ts │ ├── __tests__ │ │ └── Artboard.test.tsx │ ├── components │ │ ├── Artboard.BoxInspector.ts │ │ ├── Artboard.Canvas.tsx │ │ ├── Artboard.CanvasContent.ts │ │ ├── Artboard.Content.ts │ │ ├── Artboard.Crosshair.ts │ │ ├── Artboard.Eyedropper.ts │ │ ├── Artboard.GuideProvider.ts │ │ ├── Artboard.Guides.tsx │ │ ├── Artboard.Resizer.ts │ │ ├── Artboard.SizeInspector.ts │ │ ├── Artboard.Wrapper.ts │ │ ├── Artboard.Zoom.tsx │ │ └── Toolbar │ │ │ ├── Artboard.Toolbar.tsx │ │ │ ├── Toolbar.tsx │ │ │ ├── ToolbarButton.tsx │ │ │ └── index.ts │ └── index.ts ├── ArtboardContext │ ├── ArtboardContext.ts │ └── index.ts ├── ArtboardProvider │ ├── ArtboardProvider.tsx │ ├── __tests__ │ │ └── ArtboardProvider.test.tsx │ └── index.ts ├── BoxInspector │ ├── BoxInspector.tsx │ ├── __tests__ │ │ └── BoxInspector.test.tsx │ └── index.ts ├── Crosshair │ ├── Crosshair.Line.tsx │ ├── Crosshair.tsx │ ├── __tests__ │ │ └── Crosshair.test.tsx │ └── index.ts ├── Eyedropper │ ├── Eyedropper.js │ ├── __tests__ │ │ └── Eyedropper.test.tsx │ └── index.ts ├── Grid │ ├── Grid.tsx │ ├── __tests__ │ │ └── Grid.test.tsx │ └── index.ts ├── Guide │ ├── Guide.Container.tsx │ ├── Guide.js │ ├── Guide.utils.ts │ ├── __tests__ │ │ └── Guide.test.tsx │ └── index.ts ├── GuideContainer │ ├── GuideContainer.tsx │ ├── __tests__ │ │ └── GuideContainer.test.tsx │ └── index.ts ├── GuideContext │ ├── GuideContext.ts │ └── index.ts ├── GuideProvider │ ├── GuideProvider.tsx │ └── index.ts ├── Resizer │ ├── Resizer.tsx │ ├── __tests__ │ │ └── Resizer.test.tsx │ └── index.ts ├── SizeInspector │ ├── SizeInspector.tsx │ ├── __tests__ │ │ └── SizeInspector.test.tsx │ └── index.ts ├── UI │ ├── Base │ │ ├── Base.tsx │ │ ├── __tests__ │ │ │ └── Base.test.tsx │ │ └── index.ts │ ├── Button │ │ ├── Button.tsx │ │ ├── __tests__ │ │ │ └── Button.test.tsx │ │ └── index.ts │ ├── ButtonControl │ │ ├── ButtonControl.tsx │ │ ├── __tests__ │ │ │ └── ButtonControl.test.tsx │ │ └── index.ts │ ├── Icon │ │ ├── Box.tsx │ │ ├── Crosshair.tsx │ │ ├── Eraser.tsx │ │ ├── EyeDropper.tsx │ │ ├── Icon.test.tsx │ │ ├── IconSvg.tsx │ │ ├── Moon.tsx │ │ ├── Refresh.tsx │ │ ├── Ruler.tsx │ │ ├── Size.tsx │ │ ├── Svg.tsx │ │ ├── Zoom.tsx │ │ └── index.ts │ └── LabelText │ │ ├── LabelText.tsx │ │ └── index.ts ├── index.ts ├── testHelpers.ts ├── utils │ ├── __tests__ │ │ └── utils.test.ts │ └── index.ts └── withArtboard │ ├── __tests__ │ └── withArtboard.test.tsx │ ├── index.ts │ └── withArtboard.tsx ├── stories ├── Artboard.stories.js ├── ArtboardProvider.stories.js ├── Blue.stories.js ├── BoxInspector.stories.js ├── Crosshair.stories.js ├── Examples │ └── OptionTile.Example.js ├── Eyedropper.stories.js ├── Grid.stories.js ├── Guide.stories.js ├── Resizer.stories.js ├── SizeInspector.stories.js ├── Storybook.stories.js ├── UI.Button.stories.js └── UI.ButtonControl.stories.js └── tsconfig.json /.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": ["@helpscout/zero/babel"] 3 | } 4 | -------------------------------------------------------------------------------- /.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./node_modules/@helpscout/zero/eslint.js" 3 | } 4 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | coverage 3 | dist 4 | .opt-in 5 | .opt-out 6 | .DS_Store 7 | .eslintcache 8 | 9 | storybook-static 10 | 11 | # these cause more harm than good 12 | # when working with contributors 13 | package-lock.json 14 | yarn.lock 15 | -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | .storybook 3 | .vscode 4 | stories 5 | node_modules 6 | coverage 7 | src 8 | images 9 | scripts 10 | storybook-static 11 | testHelpers.ts 12 | __tests__ 13 | __mocks__ -------------------------------------------------------------------------------- /.prettierignore: -------------------------------------------------------------------------------- 1 | package.json 2 | node_modules 3 | dist 4 | coverage 5 | -------------------------------------------------------------------------------- /.prettierrc.yml: -------------------------------------------------------------------------------- 1 | semi: false 2 | singleQuote: true 3 | trailingComma: es5 4 | bracketSpacing: false 5 | -------------------------------------------------------------------------------- /.storybook/.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": ["@babel/preset-env", "@babel/react"], 3 | "plugins": [ 4 | "@babel/plugin-proposal-class-properties", 5 | "@babel/plugin-transform-flow-strip-types" 6 | ] 7 | } 8 | -------------------------------------------------------------------------------- /.storybook/addons.js: -------------------------------------------------------------------------------- 1 | import '@storybook/addon-knobs/register' 2 | import '@storybook/addon-actions/register' 3 | import '@storybook/addon-links/register' 4 | -------------------------------------------------------------------------------- /.storybook/config.js: -------------------------------------------------------------------------------- 1 | import {configure} from '@storybook/react' 2 | 3 | // automatically import all files ending in *.stories.js 4 | const req = require.context('../stories', true, /.stories.(js|ts|tsx)$/) 5 | function loadStories() { 6 | req.keys().forEach(filename => req(filename)) 7 | } 8 | 9 | configure(loadStories, module) 10 | -------------------------------------------------------------------------------- /.storybook/webpack.config.js: -------------------------------------------------------------------------------- 1 | const path = require('path') 2 | 3 | module.exports = (baseConfig, env, config) => { 4 | // Typescript 5 | config.module.rules.push({ 6 | test: /\.(ts|tsx)$/, 7 | loader: require.resolve('awesome-typescript-loader'), 8 | }) 9 | config.resolve.extensions.push('.ts', '.tsx') 10 | 11 | return config 12 | } 13 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | node_js: 3 | - '8' 4 | 5 | cache: 6 | directories: 7 | - node_modules 8 | 9 | install: 10 | - npm install 11 | 12 | script: 13 | - npm run test:ci 14 | - npm run build 15 | 16 | after_success: 17 | # - npm run coverage 18 | 19 | branches: 20 | only: 21 | - master 22 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2018 Help Scout 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in 13 | all copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 21 | THE SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # 🖼 Artboard 2 | 3 | [![Build Status](https://travis-ci.org/helpscout/artboard.svg?branch=master)](https://travis-ci.org/helpscout/artboard) 4 | [![npm version](https://badge.fury.io/js/%40helpscout%2Fartboard.svg)](https://badge.fury.io/js/%40helpscout%2Fartboard) 5 | 6 | > A tool kit for React UI development and design 7 | 8 | ![Artboard screenshot](./images/artboard.jpg) 9 | 10 | ## Table of contents 11 | 12 | 13 | 14 | 15 | - [Installation](#installation) 16 | - [Usage](#usage) 17 | 18 | 19 | 20 | Project is still under development! 21 | 22 | ## Installation 23 | 24 | ```text 25 | npm install --save-dev @helpscout/artboard 26 | ``` 27 | 28 | ## Usage 29 | 30 | Here's an example Storybook story with Artboard! 31 | 32 | ```jsx 33 | import React from 'react' 34 | import Artboard from '@helpscout/artboard' 35 | import MyComponent from './MyComponent' 36 | 37 | const stories = storiesOf('MyComponent', module) 38 | 39 | stories.add('Example', () => ( 40 | 41 | 42 | 43 | )) 44 | ``` 45 | -------------------------------------------------------------------------------- /images/artboard.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/helpscout/artboard/683f76e215577a5400ddcef258753ec3e02ec4b0/images/artboard.jpg -------------------------------------------------------------------------------- /images/colors.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/helpscout/artboard/683f76e215577a5400ddcef258753ec3e02ec4b0/images/colors.jpg -------------------------------------------------------------------------------- /images/guides-demo.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/helpscout/artboard/683f76e215577a5400ddcef258753ec3e02ec4b0/images/guides-demo.gif -------------------------------------------------------------------------------- /jest.config.js: -------------------------------------------------------------------------------- 1 | const jestConfig = require('@helpscout/zero/jest') 2 | 3 | const coverageList = [ 4 | 'src/**/*.{js,jsx,ts,tsx}', 5 | '!src/UI/Icon/*.{js,jsx,ts,tsx}', 6 | '!src/testHelpers.ts', 7 | ] 8 | 9 | module.exports = Object.assign({}, jestConfig, { 10 | collectCoverageFrom: [] 11 | .concat(jestConfig.collectCoverageFrom) 12 | .concat(coverageList), 13 | coverageThreshold: { 14 | global: { 15 | branches: 0, 16 | functions: 0, 17 | lines: 0, 18 | statements: 0, 19 | }, 20 | }, 21 | setupTestFrameworkScriptFile: '/scripts/setupTests.js', 22 | testMatch: [ 23 | '/src/**/__tests__/**/*.js?(x)', 24 | '/src/**/?(*.)(spec|test).js?(x)', 25 | '/src/**/?(*.)(spec|test).ts?(x)', 26 | ], 27 | testEnvironment: 'jsdom', 28 | testURL: 'http://localhost', 29 | transform: { 30 | '^.+\\.(js|jsx)$': '/node_modules/babel-jest', 31 | '^.+\\.(ts|tsx)$': 'ts-jest', 32 | }, 33 | transformIgnorePatterns: ['[/\\\\]node_modules[/\\\\].+\\.(js|jsx)$'], 34 | moduleFileExtensions: ['web.js', 'js', 'json', 'web.jsx', 'jsx', 'ts', 'tsx'], 35 | }) 36 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@helpscout/artboard", 3 | "version": "0.2.0", 4 | "description": "A tool kit for React UI development and design", 5 | "main": "dist/index.js", 6 | "private": false, 7 | "scripts": { 8 | "check:npm": "node scripts/check-npm.js", 9 | "prestart": "npm run check:npm", 10 | "build:js": "zero build", 11 | "build:ts": "tsc", 12 | "build": "npm run clean && npm run build:js && npm run build:ts", 13 | "bundle": "zero build --bundle", 14 | "lint": "zero lint", 15 | "dev": "zero test --watchAll", 16 | "git:push": "git push --follow-tags", 17 | "test": "zero test --coverage", 18 | "test:ci": "zero test", 19 | "test:update": "npm test -- --updateSnapshot --coverage", 20 | "validate": "zero validate", 21 | "setup": "npm install && npm run validate -s", 22 | "precommit": "zero precommit", 23 | "clean": "rm -rf dist lib", 24 | "coverage": "nyc report --temp-directory=coverage --reporter=text-lcov | coveralls", 25 | "release": "npm version", 26 | "version": "npm run build", 27 | "postversion": "npm publish && npm run git:push", 28 | "start": "npm run storybook", 29 | "storybook": "start-storybook -p 6007", 30 | "build-storybook": "build-storybook", 31 | "prettier": "prettier \"src/**/*.js\" --write", 32 | "pretty": "npm run prettier" 33 | }, 34 | "author": "Jon Quach (https://jonquach.com)", 35 | "repository": { 36 | "type": "git", 37 | "url": "git+https://github.com/helpscout/artboard.git" 38 | }, 39 | "bugs": { 40 | "url": "https://github.com/helpscout/artboard/issues" 41 | }, 42 | "license": "MIT", 43 | "keywords": [ 44 | "artboard", 45 | "components", 46 | "guidelines", 47 | "devkit", 48 | "guides", 49 | "gutter", 50 | "helpscout", 51 | "keyline", 52 | "react", 53 | "sketch", 54 | "storybook", 55 | "toolkit" 56 | ], 57 | "engines": { 58 | "node": ">=8" 59 | }, 60 | "peerDependencies": { 61 | "react": "^16 || ^15" 62 | }, 63 | "devDependencies": { 64 | "@babel/plugin-proposal-class-properties": "7.1.0", 65 | "@babel/plugin-transform-flow-strip-types": "7.1.6", 66 | "@babel/preset-env": "7.1.6", 67 | "@babel/preset-react": "7.0.0", 68 | "@helpscout/hsds-react": "^2.2.0", 69 | "@helpscout/zero": "^0.0.9", 70 | "@storybook/addon-actions": "^4.0.9", 71 | "@storybook/addon-knobs": "^4.0.9", 72 | "@storybook/addon-links": "^4.0.9", 73 | "@storybook/addons": "^4.0.9", 74 | "@storybook/cli": "^4.0.9", 75 | "@storybook/react": "^4.0.9", 76 | "@types/enzyme": "^3.1.14", 77 | "@types/jest": "^23.3.3", 78 | "@types/react": "^16.4.14", 79 | "@types/react-dom": "^16.0.8", 80 | "@types/react-redux": "^6.0.9", 81 | "@types/react-transition-group": "^2.0.14", 82 | "@types/redux": "^3.6.0", 83 | "awesome-typescript-loader": "^5.2.1", 84 | "babel-loader": "^8.0.4", 85 | "check-dependencies": "1.1.0", 86 | "coveralls": "3.0.2", 87 | "enzyme": "^3.7.0", 88 | "enzyme-adapter-react-16": "^1.7.0", 89 | "np": "2.20.1", 90 | "nyc": "13.0.0", 91 | "react": "^16", 92 | "react-dom": "^16", 93 | "react-scripts": "2.1.1", 94 | "react-test-renderer": "^16", 95 | "ts-jest": "^23.10.5", 96 | "typescript": "^3.2.1", 97 | "webpack": "4.26.1" 98 | }, 99 | "dependencies": { 100 | "@helpscout/fancy": "^2.1.2", 101 | "@helpscout/react-utils": "^1.0.4", 102 | "html2canvas": "1.0.0-alpha.12", 103 | "polished": "^2.3.0", 104 | "react-redux": "^5.1.1", 105 | "react-resizable": "^1.7.5", 106 | "redux": "^4.0.1", 107 | "resize-observer-polyfill": "^1.5.0" 108 | } 109 | } 110 | -------------------------------------------------------------------------------- /scripts/check-npm.js: -------------------------------------------------------------------------------- 1 | const checkDependencies = require('check-dependencies') 2 | 3 | const options = { 4 | checkGitUrls: true, 5 | install: true, 6 | packageManager: 'npm', 7 | verbose: true, 8 | } 9 | 10 | checkDependencies.sync(options) 11 | -------------------------------------------------------------------------------- /scripts/setupTests.js: -------------------------------------------------------------------------------- 1 | import {configure} from 'enzyme' 2 | import Adapter from 'enzyme-adapter-react-16' 3 | 4 | configure({adapter: new Adapter()}) 5 | -------------------------------------------------------------------------------- /src/@types/helpscout__fancy/index.d.ts: -------------------------------------------------------------------------------- 1 | declare module '@helpscout/fancy' { 2 | function styled(component: any): any 3 | export function ThemeProvider(component: any): any 4 | 5 | export default styled 6 | } 7 | -------------------------------------------------------------------------------- /src/@types/helpscout__react-utils/index.d.ts: -------------------------------------------------------------------------------- 1 | type Component = any 2 | type WrappedComponent = any 3 | type Context = { 4 | Provider: Component 5 | Consumer: Component 6 | } 7 | 8 | declare module '@helpscout/react-utils/dist/classNames' { 9 | function classNames(...classes: any): string 10 | export = classNames 11 | } 12 | 13 | declare module '@helpscout/react-utils/dist/createContext' { 14 | function createContext(value: any): Context 15 | export = createContext 16 | } 17 | 18 | declare module '@helpscout/react-utils/dist/getComponentName' { 19 | function getComponentName(component: Component): string 20 | export = getComponentName 21 | } 22 | 23 | declare module '@helpscout/react-utils/dist/getValidProps' { 24 | function getValidProps(props: Object): Object 25 | export = getValidProps 26 | } 27 | 28 | declare module '@helpscout/react-utils/dist/hoistNonReactStatics' { 29 | function hoistNonReactStatics( 30 | WrappedComponent: WrappedComponent, 31 | component: Component, 32 | ): WrappedComponent 33 | export = hoistNonReactStatics 34 | } 35 | -------------------------------------------------------------------------------- /src/Artboard/Artboard.ActionTypes.ts: -------------------------------------------------------------------------------- 1 | const ActionTypes = { 2 | // General 3 | ON_READY: 'ON_READY', 4 | LOAD_LOCAL_STATE: 'LOAD_LOCAL_STATE', 5 | SAVE_LOCAL_STATE: 'SAVE_LOCAL_STATE', 6 | PERFORM_ACTION_START: 'PERFORM_ACTION_START', 7 | PERFORM_ACTION_END: 'PERFORM_ACTION_END', 8 | RESET: 'RESET', 9 | TOGGLE_DARK_MODE: 'TOGGLE_DARK_MODE', 10 | TOGGLE_INTERFACE: 'TOGGLE_INTERFACE', 11 | NULL: 'NULL', 12 | 13 | // Zoom 14 | ZOOM: 'ZOOM', 15 | ZOOM_IN_START: 'ZOOM_IN_START', 16 | ZOOM_IN: 'ZOOM_IN', 17 | ZOOM_OUT_START: 'ZOOM_OUT_START', 18 | ZOOM_OUT: 'ZOOM_OUT', 19 | ZOOM_RESET: 'ZOOM_RESET', 20 | 21 | // Moving 22 | MOVE_START: 'MOVE_START', 23 | MOVE_END: 'MOVE_END', 24 | MOVE_DRAG_START: 'MOVE_DRAG_START', 25 | MOVE_DRAG: 'MOVE_DRAG', 26 | MOVE_DRAG_END: 'MOVE_DRAG_END', 27 | 28 | // Resizing 29 | RESIZE_ARTBOARD: 'RESIZE_ARTBOARD', 30 | 31 | // Toolbar 32 | TOGGLE_GUIDES: 'TOGGLE_GUIDES', 33 | TOGGLE_BOX_INSPECTOR: 'TOGGLE_BOX_INSPECTOR', 34 | TOGGLE_SIZE_INSPECTOR: 'TOGGLE_SIZE_INSPECTOR', 35 | 36 | EYEDROPPER_START: 'EYEDROPPER_START', 37 | EYEDROPPER_READY: 'EYEDROPPER_READY', 38 | EYEDROPPER_STOP: 'EYEDROPPER_STOP', 39 | 40 | CROSSHAIR_START: 'CROSSHAIR_START', 41 | CROSSHAIR_END: 'CROSSHAIR_END', 42 | CROSSHAIR_ADD_SNAPSHOT: 'CROSSHAIR_ADD_SNAPSHOT', 43 | CROSSHAIR_SHOW_SNAPSHOTS: 'CROSSHAIR_SHOW_SNAPSHOTS', 44 | CROSSHAIR_HIDE_SNAPSHOTS: 'CROSSHAIR_HIDE_SNAPSHOTS', 45 | CROSSHAIR_CLEAR: 'CROSSHAIR_CLEAR', 46 | } 47 | 48 | export default ActionTypes 49 | -------------------------------------------------------------------------------- /src/Artboard/Artboard.Container.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react' 2 | import ArtboardContext from '../ArtboardContext' 3 | import Artboard from './Artboard' 4 | import {getPreparedProps} from '../utils' 5 | import {defaultProps} from './Artboard.utils' 6 | 7 | class ArtboardProvider extends React.PureComponent { 8 | static defaultProps = defaultProps 9 | 10 | render() { 11 | const {children, ...rest} = this.props 12 | 13 | return ( 14 | 15 | {contextProps => { 16 | const mergedProps = getPreparedProps({...rest, ...contextProps}) 17 | 18 | return {children} 19 | }} 20 | 21 | ) 22 | } 23 | } 24 | 25 | export default ArtboardProvider 26 | -------------------------------------------------------------------------------- /src/Artboard/Artboard.KeyboardHints.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react' 2 | import styled from '@helpscout/fancy' 3 | import Base from '../UI/Base' 4 | 5 | export class KeyboardHints extends React.PureComponent { 6 | render() { 7 | return ( 8 | 9 | 10 |
11 | ⌥+D: Dark Mode 12 |
13 |
14 | Z: Zoom In 15 |
16 |
17 | ⌥+Z: Zoom Out 18 |
19 |
20 | Space: Drag 21 |
22 |
23 | G: Guides 24 |
25 |
26 | B: Box Inspector 27 |
28 |
29 | S: Size Inspector 30 |
31 |
32 | X: Crosshair 33 |
34 |
35 | : Clear Crosshairs 36 |
37 |
38 | ⌘+.: Toggle Interface 39 |
40 |
41 |
42 | ) 43 | } 44 | } 45 | 46 | const KeyboardHintsUI = styled(Base)` 47 | align-items: center; 48 | display: flex; 49 | line-height: 1; 50 | justify-content: center; 51 | flex-direction: column; 52 | opacity: 0.3; 53 | position: absolute; 54 | left: 10px; 55 | bottom: 0; 56 | pointer-events: auto; 57 | transform: translateZ(0); 58 | ` 59 | 60 | const KeyboardHintsActionsUI = styled(Base)` 61 | display: flex; 62 | flex-direction: column; 63 | flex-wrap: wrap; 64 | font-size: 10px; 65 | justify-content: center; 66 | margin-bottom: 5px; 67 | opacity: 0.5; 68 | transition: opacity 200ms linear; 69 | 70 | &:hover { 71 | opacity: 1; 72 | } 73 | 74 | * { 75 | margin: 3px 5px; 76 | } 77 | ` 78 | 79 | export default KeyboardHints 80 | -------------------------------------------------------------------------------- /src/Artboard/Artboard.actions.ts: -------------------------------------------------------------------------------- 1 | import ActionTypes from './Artboard.ActionTypes' 2 | import store from './Artboard.store' 3 | import { 4 | getArtboardNameFromProps, 5 | loadSessionState, 6 | saveSessionState, 7 | } from './Artboard.utils' 8 | 9 | /** 10 | * GENERAL 11 | */ 12 | export const onReady = props => { 13 | const state = store.getState() 14 | const mergedProps = mergePropsWithState(props, state) 15 | 16 | return { 17 | type: ActionTypes.ON_READY, 18 | payload: {props: mergedProps}, 19 | } 20 | } 21 | 22 | export const loadLocalState = props => { 23 | const state = store.getState() 24 | const artboardName = getArtboardNameFromProps(props) 25 | const localState = loadSessionState(artboardName) 26 | 27 | let mergedState = mergeStateWithProps(state, props) 28 | mergedState = mergeStateWithProps(mergedState, localState) 29 | 30 | return { 31 | type: ActionTypes.LOAD_LOCAL_STATE, 32 | payload: {state: mergedState}, 33 | } 34 | } 35 | 36 | export const saveLocalState = () => { 37 | const state = store.getState() 38 | const {artboardName} = state 39 | 40 | if (artboardName) { 41 | saveSessionState(artboardName, state) 42 | return { 43 | type: ActionTypes.SAVE_LOCAL_STATE, 44 | } 45 | } else { 46 | return { 47 | type: ActionTypes.NULL, 48 | } 49 | } 50 | } 51 | 52 | export const resetSettings = initialProps => { 53 | return { 54 | type: ActionTypes.RESET, 55 | payload: { 56 | props: initialProps, 57 | }, 58 | } 59 | } 60 | 61 | export const performActionStart = () => { 62 | return { 63 | type: ActionTypes.PERFORM_ACTION_START, 64 | } 65 | } 66 | 67 | export const performActionEnd = () => { 68 | return { 69 | type: ActionTypes.PERFORM_ACTION_END, 70 | } 71 | } 72 | 73 | /** 74 | * DARK MODE 75 | */ 76 | 77 | export const toggleDarkMode = () => { 78 | return {type: ActionTypes.TOGGLE_DARK_MODE} 79 | } 80 | 81 | /** 82 | * INTERFACE 83 | */ 84 | 85 | export const toggleInterface = () => { 86 | return {type: ActionTypes.TOGGLE_INTERFACE} 87 | } 88 | 89 | /** 90 | * GUIDES 91 | */ 92 | 93 | export const toggleGuides = () => { 94 | return {type: ActionTypes.TOGGLE_GUIDES} 95 | } 96 | 97 | /** 98 | * BOX INSPECTOR 99 | */ 100 | 101 | export const toggleBoxInspector = () => { 102 | return {type: ActionTypes.TOGGLE_BOX_INSPECTOR} 103 | } 104 | 105 | /** 106 | * SIZE INSPECTOR 107 | */ 108 | 109 | export const toggleSizeInspector = () => { 110 | return {type: ActionTypes.TOGGLE_SIZE_INSPECTOR} 111 | } 112 | 113 | /** 114 | * EYEDROPPER 115 | */ 116 | export const startEyeDropper = () => { 117 | return {type: ActionTypes.EYEDROPPER_START} 118 | } 119 | 120 | export const readyEyeDropper = () => { 121 | return {type: ActionTypes.EYEDROPPER_READY} 122 | } 123 | 124 | export const stopEyeDropper = () => { 125 | return {type: ActionTypes.EYEDROPPER_STOP} 126 | } 127 | 128 | /** 129 | * CROSSHAIR 130 | */ 131 | 132 | export const startCrosshair = () => { 133 | return {type: ActionTypes.CROSSHAIR_START} 134 | } 135 | 136 | export const stopCrosshair = () => { 137 | const state = store.getState() 138 | if (!state.isCrosshairActive) { 139 | return { 140 | type: ActionTypes.NULL, 141 | } 142 | } 143 | 144 | return {type: ActionTypes.CROSSHAIR_END} 145 | } 146 | 147 | export const toggleCrosshair = () => { 148 | const state = store.getState() 149 | 150 | if (state.isCrosshairActive) { 151 | return stopCrosshair() 152 | } else { 153 | return startCrosshair() 154 | } 155 | } 156 | 157 | export const addCrosshairSnapshot = snapshot => { 158 | return { 159 | type: ActionTypes.CROSSHAIR_ADD_SNAPSHOT, 160 | payload: { 161 | snapshot, 162 | }, 163 | } 164 | } 165 | 166 | export const clearCrosshairSnapshots = () => { 167 | return {type: ActionTypes.CROSSHAIR_CLEAR} 168 | } 169 | 170 | /** 171 | * RESIZER 172 | */ 173 | 174 | export const onResize = (event, resizeProps) => { 175 | const {height, width} = resizeProps.size 176 | 177 | return { 178 | type: ActionTypes.RESIZE_ARTBOARD, 179 | payload: { 180 | artboardHeight: height, 181 | artboardWidth: width, 182 | }, 183 | } 184 | } 185 | 186 | /** 187 | * MOVE 188 | */ 189 | export const moveStart = () => { 190 | return {type: ActionTypes.MOVE_START} 191 | } 192 | 193 | export const moveEnd = () => { 194 | return {type: ActionTypes.MOVE_END} 195 | } 196 | 197 | export const moveDragStart = () => { 198 | return {type: ActionTypes.MOVE_DRAG_START} 199 | } 200 | 201 | export const moveDrag = positions => { 202 | return {type: ActionTypes.MOVE_DRAG, payload: positions} 203 | } 204 | 205 | export const moveDragEnd = () => { 206 | return {type: ActionTypes.MOVE_DRAG_END} 207 | } 208 | 209 | /** 210 | * ZOOM 211 | */ 212 | export const zoomIn = () => { 213 | return {type: ActionTypes.ZOOM_IN} 214 | } 215 | 216 | export const zoomOut = () => { 217 | return {type: ActionTypes.ZOOM_OUT} 218 | } 219 | 220 | export const zoomInStart = () => { 221 | return {type: ActionTypes.ZOOM_IN_START} 222 | } 223 | 224 | export const zoomOutStart = () => { 225 | return {type: ActionTypes.ZOOM_OUT_START} 226 | } 227 | 228 | export const zoomReset = () => { 229 | return {type: ActionTypes.ZOOM_RESET} 230 | } 231 | 232 | /** 233 | * UTILS 234 | */ 235 | export const mergePropsWithState = (props, state): Object => { 236 | const nextState = {} 237 | Object.keys(props).forEach(key => { 238 | if (state.hasOwnProperty(key)) { 239 | const value = props[key] 240 | if (value !== undefined) { 241 | nextState[key] = props[key] 242 | } 243 | } 244 | }) 245 | 246 | return nextState 247 | } 248 | 249 | export const mergeStateWithProps = (state, props): Object => { 250 | return Object.keys(state).reduce((acc, key) => { 251 | const value = props[key] !== undefined ? props[key] : state[key] 252 | return {...acc, [key]: value} 253 | }, {}) 254 | } 255 | -------------------------------------------------------------------------------- /src/Artboard/Artboard.css.ts: -------------------------------------------------------------------------------- 1 | import styled from '@helpscout/fancy' 2 | import Base from '../UI/Base' 3 | import {cx} from '../utils' 4 | 5 | export const config = { 6 | backgroundColor: '#f2f2f2', 7 | backgroundColorDark: '#1d1a1d', 8 | color: '#000', 9 | colorDark: '#fff', 10 | } 11 | 12 | export const ArtboardWrapperUI = styled('div')` 13 | align-items: center; 14 | background-color: ${config.backgroundColor}; 15 | box-sizing: border-box; 16 | box-sizing: border-box; 17 | bottom: 0; 18 | display: flex; 19 | justify-content: center; 20 | left: 0; 21 | position: fixed; 22 | right: 0; 23 | transition: color 200ms linear, background-color 200ms linear; 24 | top: 0; 25 | user-select: none; 26 | 27 | ${({theme}) => 28 | theme.darkMode && 29 | ` 30 | background-color: ${config.backgroundColorDark}; 31 | `}; 32 | 33 | ${({isZooming}) => { 34 | if (isZooming === 'in') { 35 | return 'cursor: zoom-in;' 36 | } 37 | if (isZooming === 'out') { 38 | return 'cursor: zoom-out;' 39 | } 40 | return '' 41 | }}; 42 | 43 | ${({isMoving}) => { 44 | if (isMoving === 'start') { 45 | return 'cursor: grab;' 46 | } 47 | if (isMoving === 'dragging') { 48 | return 'cursor: grabbing;' 49 | } 50 | return '' 51 | }}; 52 | ` 53 | 54 | export const ArtboardUI = styled('div')` 55 | background: white; 56 | box-shadow: 0 1px 2px rgba(0, 0, 0, 0.2), 0 2px 8px rgba(0, 0, 0, 0.2), 57 | 0 12px 40px rgba(0, 0, 0, 0.2); 58 | box-sizing: border-box; 59 | transition: background 200ms linear, transform 200ms ease; 60 | will-change: transform; 61 | 62 | ${({theme}) => 63 | theme.darkMode && 64 | ` 65 | background-color: black; 66 | `}; 67 | 68 | ${({isPerformingAction}) => 69 | isPerformingAction && 70 | ` 71 | pointer-events: none; 72 | user-select: none; 73 | 74 | * { 75 | pointer-events: none !important; 76 | user-select: none !important; 77 | } 78 | `}; 79 | ` 80 | 81 | export const ContentUI = styled('div')` 82 | box-sizing: border-box; 83 | display: flex; 84 | min-width: 0; 85 | min-height: 0; 86 | width: 100%; 87 | height: 100%; 88 | 89 | .${cx('Resizer')} { 90 | display: flex; 91 | min-width: 0; 92 | min-height: 0; 93 | width: 100%; 94 | height: 100%; 95 | 96 | ${({alignHorizontally}) => { 97 | let justifyContent = 'center' 98 | if (alignHorizontally === 'left') { 99 | justifyContent = 'flex-start' 100 | } 101 | if (alignHorizontally === 'right') { 102 | justifyContent = 'flex-end' 103 | } 104 | 105 | return `justify-content: ${justifyContent};` 106 | }} ${({alignVertically}) => { 107 | let alignItems = 'center' 108 | if (alignVertically === 'left') { 109 | alignItems = 'flex-start' 110 | } 111 | if (alignVertically === 'right') { 112 | alignItems = 'flex-end' 113 | } 114 | 115 | return `align-items: ${alignItems};` 116 | }}; 117 | } 118 | ` 119 | 120 | export const ArtboardContentUI = styled('div')(props => ({ 121 | boxSizing: 'border-box', 122 | padding: props.padding, 123 | })) 124 | 125 | export const ArtboardBodyUI = styled('div')` 126 | box-sizing: border-box; 127 | position: relative; 128 | ` 129 | 130 | export const GenericToolBarUI = styled(Base)` 131 | align-items: center; 132 | color: ${config.color}; 133 | display: flex; 134 | justify-content: center; 135 | position: fixed; 136 | pointer-events: none; 137 | transform: translateZ(0); 138 | width: 100%; 139 | z-index: 2147483647; 140 | 141 | ${({theme}) => 142 | theme.darkMode && 143 | ` 144 | color: ${config.colorDark}; 145 | `}; 146 | ` 147 | 148 | export const InterfaceWrapperUI = styled('div')` 149 | position: static; 150 | width: 100%; 151 | ` 152 | 153 | export const ZoomWrapperUI = styled(GenericToolBarUI)` 154 | bottom: 40px; 155 | ` 156 | 157 | export const KeyboardHintsWrapperUI = styled(GenericToolBarUI)` 158 | z-index: 2147483637; 159 | bottom: 10px; 160 | ` 161 | 162 | export const ToolbarWrapperUI = styled(GenericToolBarUI)` 163 | top: 20px; 164 | ` 165 | 166 | export const ToolbarCornerUI = styled(Base)` 167 | align-items: flex-start; 168 | display: flex; 169 | justify-content: center; 170 | position: absolute; 171 | ` 172 | 173 | export const ToolbarLeftUI = styled(ToolbarCornerUI)` 174 | left: 10px; 175 | ` 176 | 177 | export const ToolbarRightUI = styled(ToolbarCornerUI)` 178 | right: 10px; 179 | ` 180 | -------------------------------------------------------------------------------- /src/Artboard/Artboard.reducers.ts: -------------------------------------------------------------------------------- 1 | import ActionTypes from './Artboard.ActionTypes' 2 | 3 | export const initialState = { 4 | artboardName: '', 5 | artboardHeight: 400, 6 | artboardWidth: 400, 7 | darkMode: false, 8 | guides: [], 9 | id: undefined, 10 | initialProps: {}, 11 | isPerformingAction: false, 12 | isCrosshairActive: false, 13 | isEyeDropperActive: false, 14 | isKeyDown: false, 15 | isMoving: undefined, 16 | isZooming: undefined, 17 | height: undefined, 18 | name: undefined, 19 | width: undefined, 20 | minWidth: undefined, 21 | minHeight: undefined, 22 | maxWidth: undefined, 23 | maxHeight: undefined, 24 | padding: 0, 25 | posX: 0, 26 | posY: 0, 27 | showGuides: true, 28 | showBoxInspector: false, 29 | showInterface: true, 30 | showSizeInspector: false, 31 | showSnapshots: true, 32 | withCenterGuides: true, 33 | withResponsiveHeight: false, 34 | withResponsiveWidth: false, 35 | snapshots: [], 36 | zoomLevel: 1, 37 | } 38 | 39 | let _showGuides = initialState.showGuides 40 | let _showSnapshots = initialState.showSnapshots 41 | let _showBoxInspector = initialState.showBoxInspector 42 | let _showSizeInspector = initialState.showSizeInspector 43 | let _zoomLevel = initialState.zoomLevel 44 | let _posX = initialState.posX 45 | let _posY = initialState.posY 46 | let initialProps = {} 47 | 48 | const ZOOM_LEVEL_MAX = 32 49 | const ZOOM_LEVEL_MIN = 0.125 50 | 51 | const reducer = (state = initialState, action) => { 52 | let nextZoom = state.zoomLevel 53 | switch (action.type) { 54 | /** 55 | * GENERAL ACTIONS 56 | */ 57 | case ActionTypes.ON_READY: 58 | initialProps = action.payload.props 59 | return { 60 | ...state, 61 | artboardHeight: null, 62 | artboardWidth: null, 63 | ...initialProps, 64 | } 65 | 66 | case ActionTypes.LOAD_LOCAL_STATE: 67 | return { 68 | ...state, 69 | ...action.payload.state, 70 | } 71 | 72 | case ActionTypes.PERFORM_ACTION_START: 73 | return { 74 | ...state, 75 | isPerformingAction: true, 76 | } 77 | 78 | case ActionTypes.PERFORM_ACTION_END: 79 | return { 80 | ...state, 81 | isPerformingAction: false, 82 | } 83 | 84 | case ActionTypes.RESET: 85 | return { 86 | ...initialState, 87 | ...initialProps, 88 | darkMode: state.darkMode, 89 | } 90 | 91 | case ActionTypes.TOGGLE_DARK_MODE: 92 | return { 93 | ...state, 94 | darkMode: !state.darkMode, 95 | } 96 | 97 | case ActionTypes.TOGGLE_INTERFACE: 98 | return { 99 | ...state, 100 | showInterface: !state.showInterface, 101 | } 102 | 103 | /** 104 | * ZOOM ACTIONS 105 | */ 106 | case ActionTypes.ZOOM_IN_START: 107 | return { 108 | ...state, 109 | isKeyDown: true, 110 | isPerformingAction: true, 111 | isZooming: 'in', 112 | } 113 | 114 | case ActionTypes.ZOOM_OUT_START: 115 | return { 116 | ...state, 117 | isKeyDown: true, 118 | isPerformingAction: true, 119 | isZooming: 'out', 120 | } 121 | 122 | case ActionTypes.ZOOM_IN: 123 | nextZoom = state.zoomLevel * 2 124 | return { 125 | ...state, 126 | zoomLevel: nextZoom > ZOOM_LEVEL_MAX ? ZOOM_LEVEL_MAX : nextZoom, 127 | } 128 | 129 | case ActionTypes.ZOOM_OUT: 130 | nextZoom = state.zoomLevel / 2 131 | return { 132 | ...state, 133 | zoomLevel: nextZoom < ZOOM_LEVEL_MIN ? ZOOM_LEVEL_MIN : nextZoom, 134 | } 135 | 136 | case ActionTypes.ZOOM_RESET: 137 | return { 138 | ...state, 139 | isZooming: undefined, 140 | } 141 | 142 | /** 143 | * MOVE ACTIONS 144 | */ 145 | case ActionTypes.MOVE_START: 146 | return { 147 | ...state, 148 | isKeyDown: true, 149 | isPerformingAction: true, 150 | isMoving: 'start', 151 | } 152 | 153 | case ActionTypes.MOVE_DRAG_START: 154 | return { 155 | ...state, 156 | isMoving: 'dragging', 157 | } 158 | 159 | case ActionTypes.MOVE_DRAG_END: 160 | return { 161 | ...state, 162 | isMoving: 'start', 163 | isKeyDown: false, 164 | } 165 | 166 | case ActionTypes.MOVE_DRAG: 167 | return { 168 | ...state, 169 | isMoving: 'dragging', 170 | posX: Math.round( 171 | state.posX + (action.payload.posX * 1) / state.zoomLevel, 172 | ), 173 | posY: Math.round( 174 | state.posY + (action.payload.posY * 1) / state.zoomLevel, 175 | ), 176 | } 177 | 178 | case ActionTypes.MOVE_END: 179 | return { 180 | ...state, 181 | isMoving: undefined, 182 | } 183 | 184 | /** 185 | * RESIZE ACTIONS 186 | */ 187 | case ActionTypes.RESIZE_ARTBOARD: 188 | return { 189 | ...state, 190 | artboardWidth: action.payload.artboardWidth, 191 | artboardHeight: action.payload.artboardHeight, 192 | } 193 | 194 | /** 195 | * TOOLBAR ACTIONS 196 | */ 197 | case ActionTypes.TOGGLE_GUIDES: 198 | return { 199 | ...state, 200 | showGuides: !state.showGuides, 201 | } 202 | 203 | case ActionTypes.TOGGLE_BOX_INSPECTOR: 204 | return { 205 | ...state, 206 | showBoxInspector: !state.showBoxInspector, 207 | } 208 | 209 | case ActionTypes.TOGGLE_SIZE_INSPECTOR: 210 | return { 211 | ...state, 212 | showSizeInspector: !state.showSizeInspector, 213 | } 214 | 215 | case ActionTypes.EYEDROPPER_START: 216 | _posX = state.posX 217 | _posY = state.posY 218 | _showGuides = state.showGuides 219 | _showSnapshots = state.showSnapshots 220 | _showBoxInspector = state.showBoxInspector 221 | _zoomLevel = state.zoomLevel 222 | 223 | return { 224 | ...state, 225 | isCrosshairActive: false, 226 | isEyeDropperActive: true, 227 | isPerformingAction: true, 228 | posX: 0, 229 | posY: 0, 230 | showGuides: false, 231 | showSnapshots: false, 232 | showBoxInspector: false, 233 | zoomLevel: 1, 234 | } 235 | 236 | case ActionTypes.EYEDROPPER_READY: 237 | return { 238 | ...state, 239 | isPerformingAction: true, 240 | } 241 | 242 | case ActionTypes.EYEDROPPER_STOP: 243 | return { 244 | ...state, 245 | isEyeDropperActive: false, 246 | isPerformingAction: false, 247 | posX: _posX, 248 | posY: _posY, 249 | showGuides: _showGuides, 250 | showBoxInspector: _showBoxInspector, 251 | showSnapshots: _showSnapshots, 252 | zoomLevel: _zoomLevel, 253 | } 254 | 255 | case ActionTypes.CROSSHAIR_START: 256 | _showBoxInspector = state.showBoxInspector 257 | _showSizeInspector = state.showSizeInspector 258 | return { 259 | ...state, 260 | isCrosshairActive: true, 261 | showSnapshots: true, 262 | showBoxInspector: false, 263 | showSizeInspector: false, 264 | } 265 | 266 | case ActionTypes.CROSSHAIR_END: 267 | return { 268 | ...state, 269 | isCrosshairActive: false, 270 | showBoxInspector: _showBoxInspector, 271 | showSizeInspector: _showSizeInspector, 272 | } 273 | 274 | case ActionTypes.CROSSHAIR_ADD_SNAPSHOT: 275 | return { 276 | ...state, 277 | snapshots: [...state.snapshots, action.payload.snapshot], 278 | } 279 | 280 | case ActionTypes.CROSSHAIR_SHOW_SNAPSHOTS: 281 | return { 282 | ...state, 283 | showSnapshots: true, 284 | } 285 | 286 | case ActionTypes.CROSSHAIR_HIDE_SNAPSHOTS: 287 | return { 288 | ...state, 289 | showSnapshots: false, 290 | } 291 | 292 | case ActionTypes.CROSSHAIR_CLEAR: 293 | return { 294 | ...state, 295 | snapshots: [], 296 | } 297 | 298 | default: 299 | return state 300 | } 301 | } 302 | 303 | export default reducer 304 | -------------------------------------------------------------------------------- /src/Artboard/Artboard.store.ts: -------------------------------------------------------------------------------- 1 | import {createStore} from 'redux' 2 | import reducers from './Artboard.reducers' 3 | 4 | const store = createStore(reducers) 5 | 6 | export default store 7 | -------------------------------------------------------------------------------- /src/Artboard/Artboard.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react' 2 | import {Provider, connect} from 'react-redux' 3 | import * as actions from './Artboard.actions' 4 | import {ThemeProvider} from '@helpscout/fancy' 5 | import store from './Artboard.store' 6 | import {defaultProps} from './Artboard.utils' 7 | import {Keys, isInputNode} from '../utils' 8 | import {saveSessionState} from './Artboard.utils' 9 | import {ZoomWrapperUI, KeyboardHintsWrapperUI, config} from './Artboard.css' 10 | import Canvas from './components/Artboard.Canvas' 11 | import Crosshair from './components/Artboard.Crosshair' 12 | import Eyedropper from './components/Artboard.Eyedropper' 13 | import KeyboardHints from './Artboard.KeyboardHints' 14 | import Toolbar from './components/Toolbar' 15 | import Wrapper from './components/Artboard.Wrapper' 16 | import Zoom from './components/Artboard.Zoom' 17 | 18 | export class Artboard extends React.Component { 19 | static defaultProps = defaultProps 20 | __bodyBackGroundColor: string | null = null 21 | 22 | constructor(props) { 23 | super(props) 24 | props.onReady(props.__initialProps) 25 | props.loadLocalState(props.__initialProps) 26 | } 27 | 28 | componentDidMount() { 29 | window.addEventListener('keydown', this.handleOnKeyDown) 30 | window.addEventListener('keyup', this.handleOnKeyUp) 31 | window.addEventListener('click', this.handleOnClick) 32 | window.addEventListener('mousedown', this.handleOnMouseDown) 33 | window.addEventListener('mouseup', this.handleOnMouseUp) 34 | window.addEventListener('mousemove', this.handleOnMouseMove) 35 | this.setBackgroundColor() 36 | } 37 | 38 | componentWillUnmount() { 39 | window.removeEventListener('keydown', this.handleOnKeyDown) 40 | window.removeEventListener('keyup', this.handleOnKeyUp) 41 | window.removeEventListener('click', this.handleOnClick) 42 | window.removeEventListener('mousedown', this.handleOnMouseDown) 43 | window.removeEventListener('mouseup', this.handleOnMouseUp) 44 | window.removeEventListener('mousemove', this.handleOnMouseMove) 45 | this.unsetBackgroundColor() 46 | 47 | this.props.saveLocalState() 48 | } 49 | 50 | shouldComponentUpdate(nextProps) { 51 | if (nextProps !== this.props) { 52 | this.props.saveLocalState() 53 | } 54 | return true 55 | } 56 | 57 | getArtboardNameFromProps = props => { 58 | return props.id || props.name 59 | } 60 | 61 | saveState = () => { 62 | const artboardName = this.getArtboardNameFromProps(this.props) 63 | if (!artboardName) return 64 | 65 | saveSessionState(artboardName, this.state) 66 | } 67 | 68 | setBackgroundColor = () => { 69 | this.__bodyBackGroundColor = document.body.style.backgroundColor 70 | document.body.style.backgroundColor = config.backgroundColor 71 | } 72 | 73 | unsetBackgroundColor = () => { 74 | document.body.style.backgroundColor = this.__bodyBackGroundColor 75 | } 76 | 77 | handleOnKeyDown = event => { 78 | if (isInputNode(event.target)) return 79 | 80 | switch (event.keyCode) { 81 | case Keys.D: 82 | if (event.altKey) { 83 | this.toggleDarkMode() 84 | } 85 | break 86 | case Keys.Z: 87 | this.stopCrosshair() 88 | if (event.altKey) { 89 | this.prepareZoomOut() 90 | } else { 91 | this.prepareZoomIn() 92 | } 93 | break 94 | case Keys.B: 95 | this.toggleBoxInspector() 96 | break 97 | case Keys.G: 98 | this.toggleGuides() 99 | break 100 | case Keys.S: 101 | this.toggleSizeInspector() 102 | break 103 | case Keys.SPACE: 104 | this.stopCrosshair() 105 | this.prepareMove() 106 | break 107 | case Keys.PERIOD: 108 | if (event.metaKey || event.ctrlKey) { 109 | this.toggleInterface() 110 | } 111 | break 112 | default: 113 | break 114 | } 115 | } 116 | 117 | handleOnKeyUp = event => { 118 | switch (event.keyCode) { 119 | case Keys.Z: 120 | this.props.zoomReset() 121 | break 122 | 123 | case Keys.X: 124 | if (!isInputNode(event.target)) { 125 | this.toggleCrosshair() 126 | } 127 | break 128 | 129 | case Keys.SPACE: 130 | this.props.moveEnd() 131 | break 132 | 133 | case Keys.ESC: 134 | this.stopCrosshair() 135 | break 136 | 137 | case Keys.BACKSPACE: 138 | if (!isInputNode(event.target)) { 139 | this.clearSnapshots() 140 | } 141 | break 142 | 143 | default: 144 | break 145 | } 146 | 147 | this.props.performActionEnd() 148 | } 149 | 150 | handleOnClick = () => { 151 | const {isZooming} = this.props 152 | 153 | if (isZooming) { 154 | if (isZooming === 'in') { 155 | this.zoomIn() 156 | } else { 157 | this.zoomOut() 158 | } 159 | } 160 | } 161 | 162 | handleOnMouseDown = () => { 163 | const {isMoving} = this.props 164 | 165 | if (isMoving && isMoving !== 'dragging') { 166 | this.props.moveDragStart() 167 | } 168 | } 169 | 170 | handleOnMouseUp = () => { 171 | const {isMoving} = this.props 172 | 173 | if (isMoving && isMoving === 'dragging') { 174 | this.props.moveDragEnd() 175 | } 176 | } 177 | 178 | handleOnMouseMove = event => { 179 | const {isMoving} = this.props 180 | 181 | if (isMoving && isMoving === 'dragging') { 182 | const {movementX, movementY} = event 183 | 184 | this.props.moveDrag({ 185 | posX: movementX, 186 | posY: movementY, 187 | }) 188 | } 189 | } 190 | 191 | toggleDarkMode = () => { 192 | this.props.toggleDarkMode() 193 | } 194 | 195 | toggleInterface = () => { 196 | this.props.toggleInterface() 197 | } 198 | 199 | prepareZoomIn = () => { 200 | if (this.props.isKeyDown && this.props.isZooming === 'in') return 201 | 202 | this.props.zoomInStart() 203 | } 204 | 205 | prepareZoomOut = () => { 206 | if (this.props.isKeyDown && this.props.isZooming === 'out') return 207 | 208 | this.props.zoomOutStart() 209 | } 210 | 211 | prepareMove = () => { 212 | if (this.props.isKeyDown && this.props.isMoving) return 213 | 214 | this.props.moveStart() 215 | } 216 | 217 | zoomIn = (event?: Event) => { 218 | if (event) { 219 | event.stopPropagation() 220 | } 221 | this.props.zoomIn() 222 | } 223 | 224 | zoomOut = (event?: Event) => { 225 | if (event) { 226 | event.stopPropagation() 227 | } 228 | this.props.zoomOut() 229 | } 230 | 231 | toggleGuides = () => { 232 | this.props.toggleGuides() 233 | } 234 | 235 | toggleBoxInspector = () => { 236 | this.props.toggleBoxInspector() 237 | } 238 | 239 | toggleSizeInspector = () => { 240 | this.props.toggleSizeInspector() 241 | } 242 | 243 | startEyeDropper = () => { 244 | this.props.startEyeDropper() 245 | } 246 | 247 | readyEyeDropper = () => { 248 | this.props.readyEyeDropper() 249 | } 250 | 251 | stopEyeDropper = () => { 252 | this.props.stopEyeDropper() 253 | } 254 | 255 | toggleCrosshair = () => { 256 | this.props.toggleCrosshair() 257 | } 258 | 259 | stopCrosshair = () => { 260 | this.props.stopCrosshair() 261 | } 262 | 263 | clearSnapshots = () => { 264 | this.props.clearCrosshairSnapshots() 265 | } 266 | 267 | render() { 268 | const {darkMode, children, showInterface} = this.props 269 | 270 | return ( 271 | 272 | 273 | 274 | 275 | {showInterface && } 276 | {children} 277 | {showInterface && } 278 | 279 | 280 | 281 | 282 | 283 | ) 284 | } 285 | } 286 | 287 | const mapStateToProps = (state, ownProps) => { 288 | const {darkMode, isKeyDown, isMoving, isZooming, showInterface} = state 289 | const artboardName = ownProps.id || ownProps.name || '' 290 | 291 | return { 292 | __initialProps: {...ownProps, artboardName}, 293 | artboardName, 294 | darkMode, 295 | isKeyDown, 296 | isMoving, 297 | isZooming, 298 | showInterface, 299 | } 300 | } 301 | 302 | const mapDispatchToProps = { 303 | loadLocalState: actions.loadLocalState, 304 | saveLocalState: actions.saveLocalState, 305 | onReady: actions.onReady, 306 | clearCrosshairSnapshots: actions.clearCrosshairSnapshots, 307 | moveStart: actions.moveStart, 308 | moveEnd: actions.moveEnd, 309 | moveDragStart: actions.moveDragStart, 310 | moveDrag: actions.moveDrag, 311 | moveDragEnd: actions.moveDragEnd, 312 | performActionStart: actions.performActionStart, 313 | performActionEnd: actions.performActionEnd, 314 | startEyeDropper: actions.startEyeDropper, 315 | readyEyeDropper: actions.readyEyeDropper, 316 | stopEyeDropper: actions.stopEyeDropper, 317 | stopCrosshair: actions.stopCrosshair, 318 | toggleCrosshair: actions.toggleCrosshair, 319 | toggleBoxInspector: actions.toggleBoxInspector, 320 | toggleDarkMode: actions.toggleDarkMode, 321 | toggleInterface: actions.toggleInterface, 322 | toggleGuides: actions.toggleGuides, 323 | toggleSizeInspector: actions.toggleSizeInspector, 324 | zoomIn: actions.zoomIn, 325 | zoomOut: actions.zoomOut, 326 | zoomInStart: actions.zoomInStart, 327 | zoomOutStart: actions.zoomOutStart, 328 | zoomReset: actions.zoomReset, 329 | } 330 | 331 | export const ConnectedArtboard = connect( 332 | mapStateToProps, 333 | mapDispatchToProps, 334 | )(Artboard) 335 | 336 | export default props => { 337 | return ( 338 | 339 | 340 | 341 | ) 342 | } 343 | -------------------------------------------------------------------------------- /src/Artboard/Artboard.types.ts: -------------------------------------------------------------------------------- 1 | import {Snapshots} from '../Crosshair/Crosshair' 2 | 3 | export interface Props { 4 | __debug: boolean 5 | alignHorizontally: 'left' | 'center' | 'right' 6 | alignVertically: 'top' | 'middle' | 'bottom' 7 | defaultHeight: number 8 | defaultWidth: number 9 | darkMode: boolean 10 | guides?: any 11 | id?: string 12 | name?: string 13 | height?: number 14 | width?: number 15 | minHeight?: number 16 | minWidth?: number 17 | maxHeight?: number 18 | maxWidth?: number 19 | padding: number 20 | posX: number 21 | posY: number 22 | showInterface: boolean 23 | snapshots: Snapshots 24 | withResponsiveHeight: boolean 25 | withResponsiveWidth: boolean 26 | withCenterGuides: boolean 27 | zoomLevel: number 28 | } 29 | 30 | export interface State { 31 | artboardName: string 32 | artboardHeight: number 33 | artboardWidth: number 34 | darkMode: boolean 35 | guides?: any 36 | isPerformingAction: boolean 37 | isCrosshairActive: boolean 38 | isEyeDropperActive: boolean 39 | isKeyDown: boolean 40 | isMoving: 'start' | 'dragging' | undefined 41 | isZooming: 'in' | 'out' | undefined 42 | showGuides: boolean 43 | showBoxInspector: boolean 44 | showInterface: boolean 45 | showSizeInspector: boolean 46 | showSnapshots: boolean 47 | snapshots: Snapshots 48 | posX: number 49 | posY: number 50 | zoomLevel: number 51 | } 52 | 53 | export type Action = { 54 | type: string 55 | payload?: any 56 | } 57 | -------------------------------------------------------------------------------- /src/Artboard/Artboard.utils.ts: -------------------------------------------------------------------------------- 1 | import {State, Action} from './Artboard.types' 2 | const LOCAL_STORAGE_KEY = '__HSDS_ARTBOARD__' 3 | 4 | export const defaultProps = { 5 | __debug: false, 6 | alignHorizontally: 'center', 7 | alignVertically: 'center', 8 | darkMode: false, 9 | defaultHeight: 280, 10 | defaultWidth: 400, 11 | padding: 0, 12 | posX: 0, 13 | posY: 0, 14 | showGuides: false, 15 | showBoxInspector: false, 16 | showInterface: false, 17 | snapshots: [], 18 | withCenterGuides: false, 19 | withResponsiveHeight: false, 20 | withResponsiveWidth: false, 21 | zoomLevel: 1, 22 | } 23 | 24 | export function getNextArtboardSize(state: State, action: Action) { 25 | const {artboardWidth: nextWidth, artboardHeight: nextHeight} = action.payload 26 | const {artboardWidth, artboardHeight, zoomLevel} = state 27 | 28 | const computedNextWidth = (nextWidth - artboardWidth) / zoomLevel 29 | const computedNextHeight = (nextHeight - artboardHeight) / zoomLevel 30 | 31 | const updatedWidth = artboardWidth + computedNextWidth 32 | const updatedHeight = artboardHeight + computedNextHeight 33 | 34 | return { 35 | artboardWidth: updatedWidth, 36 | artboardHeight: updatedHeight, 37 | } 38 | } 39 | 40 | export function getArtboardNameFromProps(props: any) { 41 | if (!props) return '' 42 | return props.id || props.name || '' 43 | } 44 | 45 | export function loadState(): Object { 46 | try { 47 | const serializedState = localStorage.getItem(LOCAL_STORAGE_KEY) 48 | if (serializedState === null) { 49 | // Save the initial state, if not defined 50 | saveState({}) 51 | return {} 52 | } 53 | 54 | return JSON.parse(serializedState) 55 | } catch (err) { 56 | return {} 57 | } 58 | } 59 | 60 | export function saveState(state: Object = {}): undefined { 61 | if (!state) return undefined 62 | 63 | try { 64 | const serializedState = JSON.stringify(state) 65 | 66 | localStorage.setItem(LOCAL_STORAGE_KEY, serializedState) 67 | 68 | return undefined 69 | } catch (err) { 70 | return undefined 71 | } 72 | } 73 | 74 | export function loadSessionState(id: string): Object { 75 | if (!id) return {} 76 | 77 | try { 78 | const localState = loadState() 79 | if (!localState || !localState[id]) { 80 | // Save the initial state, if not defined 81 | saveSessionState(id, {}) 82 | return {} 83 | } 84 | 85 | return localState[id] 86 | } catch (err) { 87 | return {} 88 | } 89 | } 90 | 91 | export function saveSessionState(id: string, state: Object): undefined { 92 | if (!id) return undefined 93 | 94 | try { 95 | const localState = loadState() 96 | if (!localState) return undefined 97 | 98 | const nextState = { 99 | ...localState, 100 | [id]: state, 101 | } 102 | 103 | saveState(nextState) 104 | 105 | return undefined 106 | } catch (err) { 107 | return undefined 108 | } 109 | } 110 | -------------------------------------------------------------------------------- /src/Artboard/__tests__/Artboard.test.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react' 2 | import {mount} from 'enzyme' 3 | import Artboard from '../index' 4 | 5 | describe('Render', () => { 6 | test('Can render component', () => { 7 | expect(mount()).toBeTruthy() 8 | }) 9 | }) 10 | -------------------------------------------------------------------------------- /src/Artboard/components/Artboard.BoxInspector.ts: -------------------------------------------------------------------------------- 1 | import {connect} from 'react-redux' 2 | import BoxInspector from '../../BoxInspector' 3 | 4 | const mapStateToProps = state => { 5 | const {showBoxInspector} = state 6 | 7 | return { 8 | showOutlines: showBoxInspector, 9 | } 10 | } 11 | 12 | export default connect(mapStateToProps)(BoxInspector) 13 | -------------------------------------------------------------------------------- /src/Artboard/components/Artboard.Canvas.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react' 2 | import {connect} from 'react-redux' 3 | import BoxInspector from './Artboard.BoxInspector' 4 | import CanvasContent from './Artboard.CanvasContent' 5 | import Content from './Artboard.Content' 6 | import GuideProvider from './Artboard.GuideProvider' 7 | import Guides from './Artboard.Guides' 8 | import Resizer from './Artboard.Resizer' 9 | import SizeInspector from './Artboard.SizeInspector' 10 | import {ArtboardUI, ArtboardBodyUI} from '../Artboard.css' 11 | import {cx} from '../../utils/index' 12 | 13 | export class Canvas extends React.Component { 14 | node: HTMLElement 15 | 16 | componentDidMount() { 17 | this.updateNodeStylesFromProps(this.props) 18 | } 19 | 20 | shouldComponentUpdate(nextProps) { 21 | // Performantly re-render the UI 22 | this.updateNodeStylesFromProps(nextProps) 23 | 24 | if (nextProps.isPerformingAction !== this.props.isPerformingAction) { 25 | return true 26 | } 27 | if (nextProps.children !== this.props.children) { 28 | return true 29 | } 30 | 31 | return false 32 | } 33 | 34 | updateNodeStylesFromProps = nextProps => { 35 | const {isMoving, posX, posY, zoomLevel} = nextProps 36 | 37 | if (!this.node) return 38 | 39 | let transition = 'background 200ms linear, transform 200ms ease' 40 | if (isMoving) { 41 | transition = 'none' 42 | } 43 | 44 | this.node.style.transform = `scale(${zoomLevel}) translate(${posX}px, ${posY}px)` 45 | this.node.style.transition = transition 46 | } 47 | 48 | setNodeRef = node => (this.node = node) 49 | 50 | render() { 51 | const {children, isPerformingAction} = this.props 52 | 53 | return ( 54 | 55 | 60 | 61 | 62 | 63 | 64 | 65 | {children} 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | ) 75 | } 76 | } 77 | 78 | const mapStateToProps = state => { 79 | const {isPerformingAction, isMoving, posX, posY, zoomLevel} = state 80 | 81 | return { 82 | isPerformingAction, 83 | isMoving, 84 | posX, 85 | posY, 86 | zoomLevel, 87 | } 88 | } 89 | 90 | export default connect(mapStateToProps)(Canvas) 91 | -------------------------------------------------------------------------------- /src/Artboard/components/Artboard.CanvasContent.ts: -------------------------------------------------------------------------------- 1 | import {connect} from 'react-redux' 2 | import {ContentUI} from '../Artboard.css' 3 | import {cx} from '../../utils' 4 | 5 | const mapStateToProps = state => { 6 | const {alignHorizontally, alignVertically} = state 7 | 8 | return { 9 | alignHorizontally, 10 | alignVertically, 11 | className: cx('ArtboardCanvasContent'), 12 | } 13 | } 14 | 15 | export default connect(mapStateToProps)(ContentUI) 16 | -------------------------------------------------------------------------------- /src/Artboard/components/Artboard.Content.ts: -------------------------------------------------------------------------------- 1 | import {connect} from 'react-redux' 2 | import {ArtboardContentUI} from '../Artboard.css' 3 | import {cx} from '../../utils' 4 | 5 | const mapStateToProps = state => { 6 | const {padding} = state 7 | 8 | return { 9 | padding, 10 | className: cx('ArtboardContent'), 11 | } 12 | } 13 | 14 | export default connect(mapStateToProps)(ArtboardContentUI) 15 | -------------------------------------------------------------------------------- /src/Artboard/components/Artboard.Crosshair.ts: -------------------------------------------------------------------------------- 1 | import {connect} from 'react-redux' 2 | import Crosshair from '../../Crosshair' 3 | import {addCrosshairSnapshot} from '../Artboard.actions' 4 | 5 | const mapStateToProps = state => { 6 | const { 7 | isCrosshairActive, 8 | showSnapshots, 9 | snapshots, 10 | posX, 11 | posY, 12 | zoomLevel, 13 | } = state 14 | 15 | return { 16 | isActive: isCrosshairActive, 17 | showSnapshots, 18 | snapshots, 19 | posX, 20 | posY, 21 | zoomLevel, 22 | } 23 | } 24 | 25 | const mapDispatchToProps = { 26 | onSnapshot: addCrosshairSnapshot, 27 | } 28 | 29 | export default connect( 30 | mapStateToProps, 31 | mapDispatchToProps, 32 | )(Crosshair) 33 | -------------------------------------------------------------------------------- /src/Artboard/components/Artboard.Eyedropper.ts: -------------------------------------------------------------------------------- 1 | import {connect} from 'react-redux' 2 | import Eyedropper from '../../Eyedropper' 3 | import {readyEyeDropper, stopEyeDropper} from '../Artboard.actions' 4 | 5 | const mapStateToProps = state => { 6 | const {isEyeDropperActive} = state 7 | 8 | return { 9 | isActive: isEyeDropperActive, 10 | } 11 | } 12 | 13 | const mapDispatchToProps = { 14 | onReady: readyEyeDropper, 15 | onStop: stopEyeDropper, 16 | } 17 | 18 | export default connect( 19 | mapStateToProps, 20 | mapDispatchToProps, 21 | )(Eyedropper) 22 | -------------------------------------------------------------------------------- /src/Artboard/components/Artboard.GuideProvider.ts: -------------------------------------------------------------------------------- 1 | import {connect} from 'react-redux' 2 | import GuideProvider from '../../GuideProvider' 3 | 4 | const mapStateToProps = state => { 5 | const {showGuides} = state 6 | 7 | return { 8 | showGuide: showGuides, 9 | } 10 | } 11 | 12 | export default connect(mapStateToProps)(GuideProvider) 13 | -------------------------------------------------------------------------------- /src/Artboard/components/Artboard.Guides.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react' 2 | import {connect} from 'react-redux' 3 | import GuideContainer from '../../GuideContainer' 4 | import Guide from '../../Guide' 5 | 6 | export class ArtboardGuides extends React.Component { 7 | static defaultProps = { 8 | guides: [], 9 | withCenterGuides: true, 10 | } 11 | 12 | render() { 13 | const {guides, withCenterGuides} = this.props 14 | let guidesMarkup = guides 15 | 16 | if (Array.isArray(guides)) { 17 | guidesMarkup = guides.map((item, index) => { 18 | const key = `guide-${index}` 19 | let props = item.props ? item.props : item 20 | 21 | if (typeof item === 'object') { 22 | return 23 | } 24 | 25 | return null 26 | }) 27 | } 28 | 29 | return ( 30 | 38 | {guidesMarkup} 39 | {withCenterGuides && [ 40 | , 47 | , 54 | ]} 55 | 56 | ) 57 | } 58 | } 59 | 60 | const mapStateToProps = state => { 61 | const {guides, withCenterGuides} = state 62 | 63 | return { 64 | guides, 65 | withCenterGuides, 66 | } 67 | } 68 | 69 | export default connect(mapStateToProps)(ArtboardGuides) 70 | -------------------------------------------------------------------------------- /src/Artboard/components/Artboard.Resizer.ts: -------------------------------------------------------------------------------- 1 | import {connect} from 'react-redux' 2 | import {onResize} from '../Artboard.actions' 3 | import Resizer from '../../Resizer' 4 | 5 | const mapStateToProps = state => { 6 | const { 7 | artboardHeight, 8 | artboardWidth, 9 | defaultWidth, 10 | defaultHeight, 11 | height, 12 | width, 13 | minWidth, 14 | minHeight, 15 | maxWidth, 16 | maxHeight, 17 | withResponsiveHeight, 18 | withResponsiveWidth, 19 | } = state 20 | 21 | return { 22 | defaultWidth: artboardWidth || defaultWidth, 23 | defaultHeight: artboardHeight || defaultHeight, 24 | height: artboardHeight || height, 25 | width: artboardWidth || width, 26 | minWidth, 27 | minHeight, 28 | maxWidth, 29 | maxHeight, 30 | withResponsiveHeight, 31 | withResponsiveWidth, 32 | } 33 | } 34 | 35 | const mapDispatchToProps = { 36 | onResizeStop: onResize, 37 | } 38 | 39 | export default connect( 40 | mapStateToProps, 41 | mapDispatchToProps, 42 | )(Resizer) 43 | -------------------------------------------------------------------------------- /src/Artboard/components/Artboard.SizeInspector.ts: -------------------------------------------------------------------------------- 1 | import {connect} from 'react-redux' 2 | import SizeInspector from '../../SizeInspector' 3 | 4 | const mapStateToProps = state => { 5 | const {showSizeInspector, zoomLevel} = state 6 | 7 | return { 8 | showOutlines: showSizeInspector, 9 | zoomLevel, 10 | } 11 | } 12 | 13 | export default connect(mapStateToProps)(SizeInspector) 14 | -------------------------------------------------------------------------------- /src/Artboard/components/Artboard.Wrapper.ts: -------------------------------------------------------------------------------- 1 | import {connect} from 'react-redux' 2 | import {ArtboardWrapperUI} from '../Artboard.css' 3 | import {cx} from '../../utils' 4 | 5 | const mapStateToProps = state => { 6 | const {id, isMoving, isZooming} = state 7 | 8 | return { 9 | className: cx('ArtboardWrapper'), 10 | id, 11 | isMoving, 12 | isZooming, 13 | } 14 | } 15 | 16 | export default connect(mapStateToProps)(ArtboardWrapperUI) 17 | -------------------------------------------------------------------------------- /src/Artboard/components/Artboard.Zoom.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react' 2 | import styled from '@helpscout/fancy' 3 | import {connect} from 'react-redux' 4 | import {zoomIn, zoomOut} from '../Artboard.actions' 5 | import Base from '../../UI/Base' 6 | import Button from '../../UI/Button' 7 | import LabelText from '../../UI/LabelText' 8 | import {noop} from '../../utils' 9 | 10 | export interface Props { 11 | onZoomIn: (event: Event) => void 12 | onZoomOut: (event: Event) => void 13 | zoomLevel: number 14 | } 15 | 16 | export class Zoom extends React.PureComponent { 17 | static defaultProps = { 18 | onZoomIn: noop, 19 | onZoomOut: noop, 20 | zoomLevel: 1, 21 | } 22 | 23 | handleOnKeyDown = event => { 24 | event.preventDefault() 25 | } 26 | 27 | render() { 28 | const {onZoomIn, onZoomOut, zoomLevel} = this.props 29 | 30 | const zoomPercentage = `${zoomLevel * 100}%` 31 | 32 | return ( 33 | 34 | 35 | 43 | 44 | {zoomPercentage} 45 | 46 | 54 | 55 | 56 | Zoom 57 | 58 | 59 | ) 60 | } 61 | } 62 | 63 | const ZoomUI = styled(Base)` 64 | align-items: center; 65 | display: flex; 66 | justify-content: center; 67 | flex-direction: column; 68 | pointer-events: none; 69 | transform: translateZ(0); 70 | z-index: 2147483647; 71 | ` 72 | 73 | const ZoomActionsUI = styled(Base)` 74 | align-items: center; 75 | display: flex; 76 | justify-content: center; 77 | margin-bottom: 5px; 78 | pointer-events: all; 79 | 80 | > * { 81 | margin: 0 3px; 82 | } 83 | ` 84 | 85 | const ZoomTextUI = styled(Base)` 86 | box-sizing: border-box; 87 | font-size: 11px; 88 | line-height: 1; 89 | ` 90 | 91 | const ZoomLevelUI = styled(ZoomTextUI)` 92 | position: relative; 93 | text-align: center; 94 | padding-left: 1px; 95 | width: 48px; 96 | ` 97 | 98 | const mapStateToProps = state => { 99 | const {zoomLevel} = state 100 | 101 | return { 102 | zoomLevel, 103 | } 104 | } 105 | 106 | const mapDispatchToProps = { 107 | onZoomIn: zoomIn, 108 | onZoomOut: zoomOut, 109 | } 110 | 111 | export default connect( 112 | mapStateToProps, 113 | mapDispatchToProps, 114 | )(Zoom) 115 | -------------------------------------------------------------------------------- /src/Artboard/components/Toolbar/Artboard.Toolbar.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react' 2 | import {connect} from 'react-redux' 3 | import * as actions from '../../Artboard.actions' 4 | import { 5 | ToolbarWrapperUI, 6 | ToolbarLeftUI, 7 | ToolbarRightUI, 8 | } from '../../Artboard.css' 9 | import Toolbar from './Toolbar' 10 | import ToolbarButton from './ToolbarButton' 11 | 12 | export class ArtboardToolbar extends React.PureComponent { 13 | toggleDarkMode = event => { 14 | event.stopPropagation() 15 | this.props.toggleDarkMode() 16 | } 17 | 18 | toggleGuides = event => { 19 | event.stopPropagation() 20 | this.props.toggleGuides() 21 | } 22 | 23 | toggleBoxInspector = event => { 24 | event.stopPropagation() 25 | this.props.toggleBoxInspector() 26 | } 27 | 28 | toggleSizeInspector = event => { 29 | event.stopPropagation() 30 | this.props.toggleSizeInspector() 31 | } 32 | 33 | startEyeDropper = event => { 34 | this.props.startEyeDropper() 35 | } 36 | 37 | toggleCrosshair = event => { 38 | event.stopPropagation() 39 | this.props.toggleCrosshair() 40 | } 41 | 42 | handleClearSnapshots = event => { 43 | event.stopPropagation() 44 | this.props.clearCrosshairSnapshots() 45 | } 46 | 47 | resetSettings = event => { 48 | event.stopPropagation() 49 | this.props.resetSettings() 50 | } 51 | 52 | render() { 53 | const { 54 | darkMode, 55 | showBoxInspector, 56 | showSizeInspector, 57 | showGuides, 58 | isCrosshairActive, 59 | } = this.props 60 | 61 | return ( 62 | 63 | 64 | 65 | 71 | 72 | 78 | 84 | 90 | 95 | 101 | 102 | 107 | 112 | 113 | 114 | 115 | ) 116 | } 117 | } 118 | 119 | const mapStateToProps = state => { 120 | const { 121 | darkMode, 122 | showBoxInspector, 123 | showSizeInspector, 124 | showGuides, 125 | isCrosshairActive, 126 | } = state 127 | 128 | return { 129 | darkMode, 130 | showBoxInspector, 131 | showSizeInspector, 132 | showGuides, 133 | isCrosshairActive, 134 | } 135 | } 136 | 137 | const mapDispatchToProps = { 138 | toggleDarkMode: actions.toggleDarkMode, 139 | toggleGuides: actions.toggleGuides, 140 | toggleBoxInspector: actions.toggleBoxInspector, 141 | toggleSizeInspector: actions.toggleSizeInspector, 142 | toggleCrosshair: actions.toggleCrosshair, 143 | startEyeDropper: actions.startEyeDropper, 144 | resetSettings: actions.resetSettings, 145 | clearCrosshairSnapshots: actions.clearCrosshairSnapshots, 146 | } 147 | 148 | export default connect( 149 | mapStateToProps, 150 | mapDispatchToProps, 151 | )(ArtboardToolbar) 152 | -------------------------------------------------------------------------------- /src/Artboard/components/Toolbar/Toolbar.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react' 2 | import styled from '@helpscout/fancy' 3 | import Base from '../../../UI/Base' 4 | import {cx} from '../../../utils' 5 | 6 | export class Toolbar extends React.PureComponent { 7 | render() { 8 | return ( 9 | 10 | {this.props.children} 11 | 12 | ) 13 | } 14 | } 15 | 16 | const ToolbarUI = styled(Base)` 17 | align-items: center; 18 | display: flex; 19 | line-height: 1; 20 | justify-content: center; 21 | flex-direction: column; 22 | pointer-events: none; 23 | z-index: 999999; 24 | ` 25 | 26 | const ToolbarContentUI = styled('div')` 27 | align-items: flex-start; 28 | display: flex; 29 | justify-content: center; 30 | margin-bottom: 5px; 31 | 32 | > * { 33 | margin: 0 10px; 34 | } 35 | ` 36 | 37 | export default Toolbar 38 | -------------------------------------------------------------------------------- /src/Artboard/components/Toolbar/ToolbarButton.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react' 2 | import styled from '@helpscout/fancy' 3 | import {cx, noop} from '../../../utils' 4 | import Base from '../../../UI/Base' 5 | import ButtonControl from '../../../UI/ButtonControl' 6 | 7 | export interface Props { 8 | label: string 9 | icon: string 10 | isActive: boolean 11 | onClick: (event: Event) => void 12 | } 13 | 14 | export class ToolbarButton extends React.PureComponent { 15 | static defaultProps = { 16 | isActive: false, 17 | label: 'Action', 18 | icon: 'Box', 19 | onClick: noop, 20 | } 21 | 22 | render() { 23 | const {isActive, label, icon, onClick} = this.props 24 | return ( 25 | 26 | 27 | 33 | 34 | 35 | ) 36 | } 37 | } 38 | 39 | const ToolbarButtonUI = styled(Base)` 40 | display: flex; 41 | align-items: center; 42 | flex-direction: column; 43 | width: 64px; 44 | flex: 1; 45 | ` 46 | 47 | const ButtonUI = styled('div')` 48 | pointer-events: auto; 49 | 50 | * { 51 | pointer-events: auto; 52 | } 53 | ` 54 | 55 | export default ToolbarButton 56 | -------------------------------------------------------------------------------- /src/Artboard/components/Toolbar/index.ts: -------------------------------------------------------------------------------- 1 | import Toolbar from './Artboard.Toolbar' 2 | 3 | export default Toolbar 4 | -------------------------------------------------------------------------------- /src/Artboard/index.ts: -------------------------------------------------------------------------------- 1 | import Artboard from './Artboard.Container' 2 | 3 | export default Artboard 4 | -------------------------------------------------------------------------------- /src/ArtboardContext/ArtboardContext.ts: -------------------------------------------------------------------------------- 1 | import createContext from '@helpscout/react-utils/dist/createContext' 2 | 3 | const Context = createContext({}) 4 | 5 | export default Context 6 | -------------------------------------------------------------------------------- /src/ArtboardContext/index.ts: -------------------------------------------------------------------------------- 1 | import ArtboardContext from './ArtboardContext' 2 | 3 | export default ArtboardContext 4 | -------------------------------------------------------------------------------- /src/ArtboardProvider/ArtboardProvider.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react' 2 | import ArtboardContext from '../ArtboardContext' 3 | 4 | class ArtboardProvider extends React.PureComponent { 5 | render() { 6 | const {children, ...rest} = this.props 7 | 8 | return ( 9 | 10 | {contextProps => { 11 | const mergedProps = { 12 | ...contextProps, 13 | ...rest, 14 | } 15 | 16 | return ( 17 | 18 | {children} 19 | 20 | ) 21 | }} 22 | 23 | ) 24 | } 25 | } 26 | 27 | export default ArtboardProvider 28 | -------------------------------------------------------------------------------- /src/ArtboardProvider/__tests__/ArtboardProvider.test.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react' 2 | import {mount} from 'enzyme' 3 | import Artboard from '../../Artboard/index' 4 | import ArtboardProvider from '../index' 5 | 6 | describe('Render', () => { 7 | test('Can render component', () => { 8 | expect( 9 | mount( 10 | 11 | 12 | , 13 | ), 14 | ).toBeTruthy() 15 | }) 16 | }) 17 | -------------------------------------------------------------------------------- /src/ArtboardProvider/index.ts: -------------------------------------------------------------------------------- 1 | import ArtboardProvider from './ArtboardProvider' 2 | 3 | export default ArtboardProvider 4 | -------------------------------------------------------------------------------- /src/BoxInspector/BoxInspector.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react' 2 | import {cx} from '../utils' 3 | import styled from '@helpscout/fancy' 4 | 5 | export interface Props { 6 | outline: string 7 | showOutlines: boolean 8 | targetSelector: string 9 | } 10 | 11 | export class BoxInspector extends React.Component { 12 | static defaultProps = { 13 | targetSelector: '*', 14 | outline: '1px solid red', 15 | showOutlines: true, 16 | } 17 | 18 | node: HTMLDivElement 19 | 20 | componentDidMount() { 21 | this.bindEvents() 22 | } 23 | 24 | componentWillUnmount() { 25 | this.unbindEvents() 26 | } 27 | 28 | componentDidUpdate() { 29 | this.unbindEvents() 30 | this.bindEvents() 31 | if (!this.props.showOutlines) { 32 | this.cleanUp() 33 | } 34 | } 35 | 36 | bindEvents = () => { 37 | if (!this.node) return 38 | if (!this.props.showOutlines) return 39 | 40 | Array.from(this.node.querySelectorAll(this.props.targetSelector)).forEach( 41 | node => { 42 | node.addEventListener('mouseenter', this.handleOnMouseEnter) 43 | node.addEventListener('mouseleave', this.handleOnMouseLeave) 44 | }, 45 | ) 46 | } 47 | 48 | unbindEvents = () => { 49 | if (!this.node) return 50 | 51 | Array.from(this.node.querySelectorAll(this.props.targetSelector)).forEach( 52 | node => { 53 | node.removeEventListener('mouseenter', this.handleOnMouseEnter) 54 | node.removeEventListener('mouseleave', this.handleOnMouseLeave) 55 | }, 56 | ) 57 | } 58 | 59 | cleanUp = () => { 60 | if (!this.node) return 61 | 62 | Array.from(this.node.querySelectorAll(this.props.targetSelector)).forEach( 63 | node => { 64 | if (node['style']) { 65 | node['style'].outline = null 66 | } 67 | }, 68 | ) 69 | } 70 | 71 | handleOnMouseEnter = event => { 72 | event.target.style.outline = this.props.outline 73 | } 74 | 75 | handleOnMouseLeave = event => { 76 | event.target.style.outline = 'none' 77 | } 78 | 79 | setNodeRef = node => (this.node = node) 80 | 81 | render() { 82 | return ( 83 | 88 | {this.props.children} 89 | 90 | ) 91 | } 92 | } 93 | 94 | const BoxInspectorUI = styled('div')` 95 | ${({showOutlines}) => 96 | showOutlines && 97 | ` 98 | * { 99 | pointer-events: auto !important; 100 | } 101 | 102 | [class*="${cx()}"], 103 | .${cx('Guide')} * { 104 | pointer-events: none !important; 105 | } 106 | `}; 107 | ` 108 | 109 | export default BoxInspector 110 | -------------------------------------------------------------------------------- /src/BoxInspector/__tests__/BoxInspector.test.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react' 2 | import {mount} from 'enzyme' 3 | import BoxInspector from '../index' 4 | 5 | describe('Render', () => { 6 | test('Can render component', () => { 7 | expect(mount()).toBeTruthy() 8 | }) 9 | }) 10 | -------------------------------------------------------------------------------- /src/BoxInspector/index.ts: -------------------------------------------------------------------------------- 1 | import BoxInspector from './BoxInspector' 2 | 3 | export default BoxInspector 4 | -------------------------------------------------------------------------------- /src/Crosshair/Crosshair.Line.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react' 2 | import styled from '@helpscout/fancy' 3 | 4 | export class Line extends React.Component { 5 | static defaultProps = { 6 | centerCoords: {x: 0, y: 0}, 7 | color: 'cyan', 8 | coordinate: 'x', 9 | opacity: 1, 10 | isActive: false, 11 | posX: 0, 12 | posY: 0, 13 | x: 0, 14 | y: 0, 15 | } 16 | 17 | node: HTMLElement 18 | labelNode: HTMLElement 19 | 20 | componentDidMount() { 21 | this.updateNodeStylesFromProps(this.props) 22 | } 23 | 24 | shouldComponentUpdate(nextProps) { 25 | this.updateNodeStylesFromProps(nextProps) 26 | return false 27 | } 28 | 29 | isX = () => { 30 | return this.props.coordinate === 'x' 31 | } 32 | 33 | updateNodeStylesFromProps = props => { 34 | const values = getValueFromProps(props) 35 | let value 36 | 37 | if (this.isX()) { 38 | value = values.x 39 | } else { 40 | value = values.y 41 | } 42 | 43 | const transform = this.isX() 44 | ? `translateY(${value}px)` 45 | : `translateX(${value}px)` 46 | 47 | this.node.style.transform = transform 48 | } 49 | 50 | // Disabled for now (For performance reasons) 51 | updateLabelValue = value => { 52 | this.labelNode.innerHTML = `${value}px` 53 | } 54 | 55 | getComponent = () => { 56 | return this.isX() ? LineXUI : LineYUI 57 | } 58 | 59 | setNodeRef = node => (this.node = node) 60 | setLabelNodeRef = node => (this.labelNode = node) 61 | 62 | render() { 63 | const {className, color, opacity} = this.props 64 | const Component = this.getComponent() 65 | 66 | return ( 67 | 68 | 69 | 70 | ) 71 | } 72 | } 73 | 74 | export default Line 75 | 76 | const LineBaseUI = styled('div')` 77 | position: absolute; 78 | top: 0; 79 | left: 0; 80 | will-change: transform; 81 | 82 | ${({color}) => ` 83 | color: ${color}; 84 | background-color: currentColor; 85 | `}; 86 | 87 | ${({opacity}) => ` 88 | opacity: ${opacity}; 89 | `}; 90 | ` 91 | 92 | LineBaseUI.defaultProps = { 93 | opacity: 1, 94 | } 95 | 96 | const LineXUI = styled(LineBaseUI)` 97 | width: 100%; 98 | height: 1px; 99 | ` 100 | 101 | const LineYUI = styled(LineBaseUI)` 102 | height: 100%; 103 | width: 1px; 104 | ` 105 | 106 | const LabelUI = styled('div')` 107 | color: currentColor; 108 | font-size: 11px; 109 | padding-top: 2px; 110 | padding-left: 2px; 111 | will-change: contents; 112 | ` 113 | 114 | export const getValueFromProps = (props): {x: number; y: number} => { 115 | const {isActive, centerCoords, x, y, posX, posY, zoomLevel} = props 116 | let computedX = posY + y 117 | let computedY = posX + x 118 | 119 | if (!isActive) { 120 | computedX = 121 | (centerCoords.y - y) * zoomLevel * -1 + centerCoords.y + posY * zoomLevel 122 | computedY = 123 | (centerCoords.x - x) * zoomLevel * -1 + centerCoords.x + posX * zoomLevel 124 | } 125 | 126 | return { 127 | x: computedX, 128 | y: computedY, 129 | } 130 | } 131 | -------------------------------------------------------------------------------- /src/Crosshair/Crosshair.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react' 2 | import styled from '@helpscout/fancy' 3 | import Line from './Crosshair.Line' 4 | import Base from '../UI/Base' 5 | import {cx, Keys} from '../utils' 6 | 7 | export type Snapshot = { 8 | x: number 9 | y: number 10 | } 11 | 12 | export type ViewportCoordinates = { 13 | height: number 14 | width: number 15 | x: number 16 | y: number 17 | } 18 | 19 | export type Snapshots = Array 20 | 21 | export interface Props { 22 | color: string 23 | isActive: boolean 24 | onSnapshot?: (snapshot: Snapshot) => void 25 | posX: number 26 | posY: number 27 | showSnapshots: boolean 28 | snapshotOpacity: number 29 | snapshots: Snapshots 30 | style?: Object 31 | zoomLevel: number 32 | } 33 | 34 | export interface State { 35 | centerCoords: ViewportCoordinates 36 | isActive: boolean 37 | x: number 38 | y: number 39 | snapshots: Snapshots 40 | } 41 | 42 | export class Crosshair extends React.PureComponent { 43 | static defaultProps = { 44 | color: 'cyan', 45 | isActive: true, 46 | posX: 0, 47 | posY: 0, 48 | snapshots: [], 49 | snapshotOpacity: 0.4, 50 | showSnapshots: true, 51 | style: {}, 52 | zoomLevel: 1, 53 | } 54 | 55 | constructor(props) { 56 | super(props) 57 | 58 | this.state = { 59 | centerCoords: this.getCenterCoords(), 60 | isActive: props.isActive, 61 | x: 0, 62 | y: 0, 63 | snapshots: props.snapshots, 64 | } 65 | } 66 | 67 | componentDidMount() { 68 | window.addEventListener('mousemove', this.setCoordinates) 69 | window.addEventListener('click', this.addSnapshot) 70 | window.addEventListener('keyup', this.stopCrosshair) 71 | window.addEventListener('resize', this.setCenterCoords) 72 | } 73 | 74 | componentWillUnmount() { 75 | window.removeEventListener('mousemove', this.setCoordinates) 76 | window.removeEventListener('click', this.addSnapshot) 77 | window.removeEventListener('keyup', this.stopCrosshair) 78 | window.removeEventListener('resize', this.setCenterCoords) 79 | } 80 | 81 | componentWillReceiveProps(nextProps) { 82 | if (nextProps.isActive !== this.state.isActive) { 83 | this.setState({ 84 | isActive: nextProps.isActive, 85 | }) 86 | } 87 | if (nextProps.snapshots !== this.state.snapshots) { 88 | this.setState({ 89 | snapshots: nextProps.snapshots, 90 | }) 91 | } 92 | } 93 | 94 | getCenterCoords = (): ViewportCoordinates => { 95 | // @ts-ignore 96 | const {innerHeight, innerWidth} = document.defaultView 97 | 98 | return { 99 | height: innerHeight, 100 | width: innerWidth, 101 | x: Math.round(innerWidth / 2), 102 | y: Math.round(innerHeight / 2), 103 | } 104 | } 105 | 106 | setCenterCoords = () => { 107 | this.setState({ 108 | centerCoords: this.getCenterCoords(), 109 | }) 110 | } 111 | 112 | handleOnKeyUp = event => { 113 | if (event.keyCode === Keys.ESC) { 114 | this.stopCrosshair() 115 | } 116 | } 117 | 118 | setCoordinates = event => { 119 | const {x, y} = event 120 | 121 | this.setState({ 122 | x, 123 | y, 124 | }) 125 | } 126 | 127 | addSnapshot = () => { 128 | const {posX, posY, zoomLevel} = this.props 129 | const {centerCoords, isActive, x, y, snapshots} = this.state 130 | 131 | if (!isActive) return 132 | 133 | const nextX = Math.round( 134 | centerCoords.x + (x - centerCoords.x) / zoomLevel - posX, 135 | ) 136 | const nextY = Math.round( 137 | centerCoords.y + (y - centerCoords.y) / zoomLevel - posY, 138 | ) 139 | 140 | const snap = { 141 | x: nextX, 142 | y: nextY, 143 | } 144 | 145 | if (this.props.onSnapshot) { 146 | this.props.onSnapshot(snap) 147 | } else { 148 | this.setState({ 149 | snapshots: [...snapshots, snap], 150 | }) 151 | } 152 | } 153 | 154 | stopCrosshair = () => { 155 | this.setState({ 156 | isActive: false, 157 | }) 158 | } 159 | 160 | getLineProps = () => { 161 | const {color, posX, posY, snapshotOpacity, zoomLevel} = this.props 162 | const {centerCoords} = this.state 163 | 164 | return { 165 | centerCoords, 166 | color, 167 | posX, 168 | posY, 169 | opacity: snapshotOpacity, 170 | snapshotOpacity, 171 | zoomLevel, 172 | } 173 | } 174 | 175 | renderSnapshots = () => { 176 | const {showSnapshots} = this.props 177 | const {snapshots} = this.state 178 | 179 | if (!showSnapshots) return null 180 | 181 | return snapshots.map((snap: Snapshot, index) => { 182 | const {x, y} = snap 183 | 184 | return ( 185 |
186 | 194 | 202 |
203 | ) 204 | }) 205 | } 206 | 207 | render() { 208 | const {style} = this.props 209 | const {x, y, isActive} = this.state 210 | 211 | return ( 212 | 213 | {isActive && ( 214 |
215 | 226 | 237 |
238 | )} 239 | {this.renderSnapshots()} 240 |
241 | ) 242 | } 243 | } 244 | 245 | const CrosshairUI = styled(Base)` 246 | pointer-events: none; 247 | position: fixed; 248 | z-index: 999999; 249 | top: 0; 250 | left: 0; 251 | right: 0; 252 | bottom: 0; 253 | 254 | * { 255 | pointer-events: none; 256 | } 257 | ` 258 | 259 | export default Crosshair 260 | -------------------------------------------------------------------------------- /src/Crosshair/__tests__/Crosshair.test.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react' 2 | import {mount} from 'enzyme' 3 | import Crosshair from '../index' 4 | 5 | describe('Render', () => { 6 | test('Can render component', () => { 7 | expect(mount()).toBeTruthy() 8 | }) 9 | }) 10 | -------------------------------------------------------------------------------- /src/Crosshair/index.ts: -------------------------------------------------------------------------------- 1 | import Crosshair from './Crosshair' 2 | 3 | export default Crosshair 4 | -------------------------------------------------------------------------------- /src/Eyedropper/Eyedropper.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import html2canvas from 'html2canvas' 3 | import styled from '@helpscout/fancy' 4 | import {cx, noop, Keys} from '../utils' 5 | 6 | const GRID_PATTERN = 7 | 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAcAAAAHCAYAAADEUlfTAAAAGElEQVQYV2NkYGD4z4ADMEIlQTQGGHSSABNFBwdl5cZ4AAAAAElFTkSuQmCC' 8 | 9 | export class Eyedropper extends React.Component { 10 | static defaultProps = { 11 | __debug: false, 12 | isActive: true, 13 | onPickColor: noop, 14 | onStart: noop, 15 | onReady: noop, 16 | onStop: noop, 17 | previewSize: 100, 18 | previewScale: 5, 19 | } 20 | 21 | state = { 22 | canvas: undefined, 23 | color: undefined, 24 | isInPreviewMode: false, 25 | mouseX: 0, 26 | mouseY: 0, 27 | bodyOffsetX: 0, 28 | bodyOffsetY: 0, 29 | isProcessing: false, 30 | } 31 | 32 | _isMounted = false 33 | 34 | componentDidMount() { 35 | this._isMounted = true 36 | 37 | window.addEventListener('click', this.handleOnClick) 38 | window.addEventListener('mousemove', this.colorPreview) 39 | window.addEventListener('keyup', this.handleOnKeyUp) 40 | } 41 | 42 | componentWillUnmount() { 43 | this._isMounted = false 44 | window.removeEventListener('click', this.handleOnClick) 45 | window.removeEventListener('mousemove', this.colorPreview) 46 | window.removeEventListener('keyup', this.handleOnKeyUp) 47 | 48 | this.closePreview() 49 | } 50 | 51 | safeSetState = (state, callback) => { 52 | if (!this._isMounted) return 53 | this.setState(state, props => { 54 | if (callback && typeof callback === 'function') { 55 | callback(props) 56 | } 57 | }) 58 | } 59 | 60 | handleOnClick = event => { 61 | if (!this.state.isInPreviewMode) { 62 | this.startPreviewMode(event) 63 | } else { 64 | this.stopPreviewMode() 65 | } 66 | } 67 | 68 | handleOnKeyUp = event => { 69 | if (this.state.isInPreviewMode) { 70 | if (event.keyCode === Keys.ESC) { 71 | this.closePreview() 72 | } 73 | } 74 | } 75 | 76 | startPreviewMode = (event = {x: 0, y: 0}) => { 77 | if (!this.props.isActive) return 78 | if (this.state.isProcessing) return 79 | 80 | const {x, y} = event 81 | 82 | this.props.onStart() 83 | 84 | this.safeSetState( 85 | { 86 | isProcessing: true, 87 | }, 88 | () => { 89 | html2canvas(document.body, { 90 | logging: this.props.__debug, 91 | scale: 1, 92 | }).then(canvas => { 93 | document.body.style.cursor = 'none' 94 | 95 | this.props.onReady() 96 | 97 | this.safeSetState({ 98 | canvas, 99 | isInPreviewMode: true, 100 | isProcessing: false, 101 | mouseX: x, 102 | mouseY: y, 103 | }) 104 | }) 105 | }, 106 | ) 107 | } 108 | 109 | stopPreviewMode = () => { 110 | this.copyColorToClipboard() 111 | console.log(`Selected color ${this.state.color}`) 112 | this.closePreview() 113 | } 114 | 115 | closePreview = event => { 116 | document.body.style.cursor = null 117 | this.props.onStop(this.state.color) 118 | 119 | this.safeSetState({ 120 | color: undefined, 121 | isInPreviewMode: false, 122 | }) 123 | } 124 | 125 | colorPreview = event => { 126 | if (!this.state.canvas) return 127 | 128 | const {x, y} = event 129 | const color = getColorFromCanvas(this.state.canvas, x, y) 130 | 131 | this.renderPreviewCanvas(x, y) 132 | 133 | this.safeSetState({ 134 | color, 135 | mouseX: x, 136 | mouseY: y, 137 | }) 138 | } 139 | 140 | copyColorToClipboard = () => { 141 | const el = document.createElement('textarea') 142 | el.value = this.state.color 143 | document.body.appendChild(el) 144 | el.select() 145 | document.execCommand('copy') 146 | document.body.removeChild(el) 147 | } 148 | 149 | renderPreviewCanvas = (x, y) => { 150 | if (!this.state.canvas || !this.previewCanvas) return 151 | if (!this.state.isInPreviewMode) return 152 | 153 | const {previewSize, previewScale} = this.props 154 | const context = this.previewCanvas.getContext('2d') 155 | const canvasSize = Math.round(previewSize / previewScale) 156 | const offsetPreviewSize = Math.round(canvasSize / 2) 157 | const accuracyBuffer = 0.5 // For non-2x resolution displays 158 | 159 | context.drawImage( 160 | this.state.canvas, 161 | x - offsetPreviewSize + accuracyBuffer, 162 | y - offsetPreviewSize + accuracyBuffer, 163 | canvasSize, 164 | canvasSize, 165 | 0, 166 | 0, 167 | canvasSize, 168 | canvasSize, 169 | ) 170 | } 171 | 172 | getColorPreviewStyles = () => { 173 | const {color, isInPreviewMode, mouseX, mouseY} = this.state 174 | 175 | const boxShadow = ` 176 | 0 0 0 3px ${color}, 177 | 0 0 0 4px white, 178 | 0 2px 4px rgba(0, 0, 0, 0.1), 179 | 0 0px 12px 3px rgba(0, 0, 0, 0.3), 180 | 0 8px 20px rgba(0, 0, 0, 0.2) 181 | ` 182 | 183 | return { 184 | boxShadow, 185 | display: isInPreviewMode ? 'block' : 'none', 186 | transform: `translate(${mouseX - 50}px,${mouseY - 50}px)`, 187 | } 188 | } 189 | 190 | setPreviewNodeRef = node => (this.previewCanvas = node) 191 | 192 | render() { 193 | const {previewSize, previewScale} = this.props 194 | const {color} = this.state 195 | 196 | const previewNodeSize = Math.round(previewSize / previewScale) 197 | 198 | return ( 199 | 203 | 204 | 212 | 213 | {color} 214 | 215 | ) 216 | } 217 | } 218 | 219 | function getColorFromCanvas(canvas, x, y) { 220 | const croppedCanvas = document.createElement('canvas') 221 | const croppedCanvasContext = croppedCanvas.getContext('2d') 222 | 223 | croppedCanvas.width = 1 224 | croppedCanvas.height = 1 225 | 226 | croppedCanvasContext.drawImage(canvas, x, y, 1, 1, 0, 0, 1, 1) 227 | 228 | const colorRawValues = croppedCanvasContext.getImageData(0, 0, 1, 1).data 229 | const hexColor = getHexFromRawCanvasColor(colorRawValues) 230 | 231 | return hexColor 232 | } 233 | 234 | function rgbToHex(r, g, b) { 235 | if (r > 255 || g > 255 || b > 255) return 236 | return ((r << 16) | (g << 8) | b).toString(16) 237 | } 238 | 239 | function getHexFromRawCanvasColor(rawCanvasColor) { 240 | return ( 241 | '#' + 242 | ( 243 | '000000' + 244 | rgbToHex(rawCanvasColor[0], rawCanvasColor[1], rawCanvasColor[2]) 245 | ).slice(-6) 246 | ) 247 | } 248 | 249 | const ColorPreviewUI = styled('div')` 250 | box-sizing: border-box; 251 | border: 3px solid white; 252 | border-radius: 9999px; 253 | position: fixed; 254 | z-index: 999999; 255 | top: 0; 256 | left: 0; 257 | will-change: transform, box-shadow; 258 | 259 | ${({previewSize}) => ` 260 | height: ${previewSize}px; 261 | width: ${previewSize}px; 262 | `} * { 263 | box-sizing: border-box; 264 | } 265 | ` 266 | 267 | const CrosshairUI = styled('div')` 268 | box-sizing: border-box; 269 | border: 1px solid white; 270 | box-shadow: 0 0 3px 1px rgba(0, 0, 0, 0.2); 271 | border-radius: 9999px; 272 | height: 6px; 273 | width: 6px; 274 | position: absolute; 275 | top: 50%; 276 | left: 50%; 277 | margin: -3px 0 0 -3px; 278 | z-index: 999999; 279 | ` 280 | 281 | const LabelUI = styled('div')` 282 | border-radius: 4px; 283 | background: rgba(0, 0, 0, 0.7); 284 | color: white; 285 | font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Helvetica, Arial, 286 | sans-serif, 'Apple Color Emoji', 'Segoe UI Emoji', 'Segoe UI Symbol'; 287 | font-size: 10px; 288 | line-height: 1; 289 | padding: 4px 4px; 290 | position: absolute; 291 | bottom: 13px; 292 | width: 54px; 293 | left: 50%; 294 | margin-left: -27px; 295 | text-align: center; 296 | z-index: 999999; 297 | ` 298 | 299 | const PreviewCanvasUI = styled('canvas')` 300 | display: block; 301 | border-radius: 99999px; 302 | position: absolute; 303 | top: 50%; 304 | left: 50%; 305 | z-index: -1; 306 | 307 | ${({previewSize, previewScale}) => { 308 | const canvasSize = Math.round(previewSize / previewScale) 309 | 310 | return ` 311 | height: ${canvasSize}px; 312 | width: ${canvasSize}px; 313 | transform: translate(-50%, -50%) scale(${previewScale}); 314 | ` 315 | }}; 316 | ` 317 | 318 | const GridUI = styled('div')` 319 | background: url(${GRID_PATTERN}) 1px 1px repeat; 320 | position: absolute; 321 | opacity: 0.2; 322 | top: 0; 323 | left: 0; 324 | border-radius: 9999px; 325 | width: 100%; 326 | height: 100%; 327 | z-index: 999999; 328 | ` 329 | 330 | export default Eyedropper 331 | -------------------------------------------------------------------------------- /src/Eyedropper/__tests__/Eyedropper.test.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react' 2 | import {mount} from 'enzyme' 3 | import Eyedropper from '../index' 4 | 5 | describe('Render', () => { 6 | test('Can render component', () => { 7 | expect(mount()).toBeTruthy() 8 | }) 9 | }) 10 | -------------------------------------------------------------------------------- /src/Eyedropper/index.ts: -------------------------------------------------------------------------------- 1 | import Eyedropper from './Eyedropper' 2 | 3 | export default Eyedropper 4 | -------------------------------------------------------------------------------- /src/Grid/Grid.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react' 2 | import styled from '@helpscout/fancy' 3 | import GuideContainer from '../GuideContainer' 4 | import GuideProvider from '../GuideProvider' 5 | import Guide from '../Guide' 6 | import {cx} from '../utils' 7 | 8 | class Grid extends React.PureComponent { 9 | static defaultProps = { 10 | maxWidth: 1020, 11 | margin: 'auto', 12 | gutter: 15, 13 | grid: 12, 14 | } 15 | 16 | getGuideProps = (): Object => { 17 | const {gutter} = this.props 18 | const gutterOffset = gutter / 2 19 | 20 | return { 21 | flex: 1, 22 | minWidth: 0, 23 | maxWidth: '100%', 24 | position: 'relative', 25 | marginLeft: gutterOffset, 26 | marginRight: gutterOffset, 27 | width: 'auto', 28 | } 29 | } 30 | 31 | getContainerProps = (): Object => { 32 | const {grid, gutter, ...rest} = this.props 33 | 34 | return { 35 | ...rest, 36 | margin: 'auto', 37 | } 38 | } 39 | 40 | getRowProps = (): Object => { 41 | const {gutter} = this.props 42 | 43 | const gutterOffset = (gutter / 2) * -1 44 | 45 | return { 46 | display: 'flex', 47 | marginLeft: gutterOffset, 48 | marginRight: gutterOffset, 49 | height: '100%', 50 | minWidth: 0, 51 | width: '100%', 52 | } 53 | } 54 | 55 | renderGuides = (): Array => { 56 | const {grid} = this.props 57 | 58 | return [...Array.from(Array(grid).keys())].map((item, index) => ( 59 | 60 | )) 61 | } 62 | 63 | render() { 64 | return ( 65 | 66 | 67 | 68 | {this.renderGuides()} 69 | 70 | 71 | 72 | ) 73 | } 74 | } 75 | 76 | const RowUI = styled('div')(({children, ...props}) => props) 77 | 78 | export default Grid 79 | -------------------------------------------------------------------------------- /src/Grid/__tests__/Grid.test.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react' 2 | import {mount} from 'enzyme' 3 | import Grid from '../index' 4 | 5 | describe('Render', () => { 6 | test('Can render component', () => { 7 | expect(mount()).toBeTruthy() 8 | }) 9 | }) 10 | -------------------------------------------------------------------------------- /src/Grid/index.ts: -------------------------------------------------------------------------------- 1 | import Grid from './Grid' 2 | 3 | export default Grid 4 | -------------------------------------------------------------------------------- /src/Guide/Guide.Container.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react' 2 | import GuideContext from '../GuideContext' 3 | import {getPreparedProps} from '../utils' 4 | import {defaultProps} from './Guide.utils' 5 | 6 | class GuideContainer extends React.PureComponent { 7 | static defaultProps = defaultProps 8 | 9 | render() { 10 | const {children, ...rest} = this.props 11 | 12 | return ( 13 | 14 | {contextProps => { 15 | const mergedProps = getPreparedProps({...rest, ...contextProps}) 16 | // @ts-ignore 17 | const {showGuide} = mergedProps 18 | 19 | if (!showGuide) return
20 | 21 | return children 22 | }} 23 | 24 | ) 25 | } 26 | } 27 | 28 | export default GuideContainer 29 | -------------------------------------------------------------------------------- /src/Guide/Guide.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import styled from '@helpscout/fancy' 3 | import classNames from '@helpscout/react-utils/dist/classNames' 4 | import ResizeObserver from 'resize-observer-polyfill' 5 | import GuideContext from '../GuideContext' 6 | import Container from './Guide.Container' 7 | import {rgba} from 'polished' 8 | import {cx, getPreparedProps} from '../utils' 9 | import {defaultProps} from './Guide.utils' 10 | 11 | class Guide extends React.PureComponent { 12 | static defaultProps = defaultProps 13 | 14 | node: HTMLElement 15 | resizeObserver: Observable 16 | 17 | constructor(props) { 18 | super(props) 19 | 20 | this.state = { 21 | width: props.width, 22 | height: props.height, 23 | } 24 | 25 | this.resizeObserver = new ResizeObserver(this.resizeHandler) 26 | } 27 | 28 | componentDidMount() { 29 | /* istanbul ignore else */ 30 | if (this.node) { 31 | this.resizeObserver.observe(this.node) 32 | } 33 | } 34 | 35 | componentWillUnmount() { 36 | /* istanbul ignore next */ 37 | if (this.node) { 38 | this.resizeObserver.unobserve(this.node) 39 | } 40 | } 41 | 42 | /* istanbul ignore next */ 43 | resizeHandler = entries => { 44 | for (const entry of entries) { 45 | const {width, height} = entry.contentRect 46 | 47 | this.setState({ 48 | height: Math.round(height), 49 | width: Math.round(width), 50 | }) 51 | } 52 | } 53 | 54 | setNodeRef = node => (this.node = node) 55 | 56 | render() { 57 | const {className, children, showGuide, ...rest} = this.props 58 | const {height, width} = this.state 59 | 60 | return ( 61 | 62 | {contextProps => { 63 | const mergedProps = getPreparedProps({...rest, ...contextProps}) 64 | // @ts-ignore 65 | const {showValues} = mergedProps 66 | 67 | return ( 68 | 73 | {showValues && ( 74 |
75 | 76 | {height} 77 | 78 | {width} 79 |
80 | )} 81 |
82 | ) 83 | }} 84 |
85 | ) 86 | } 87 | } 88 | 89 | const GuideUI = styled('div')(({children, ...props}) => ({ 90 | ...props, 91 | background: rgba(props.color, props.opacity), 92 | fontFamily: 93 | '"SFMono-Regular",Consolas,"Liberation Mono",Menlo,Courier,monospace', 94 | fontSize: 8, 95 | lineHeight: 1, 96 | opacity: 1, 97 | '*': { 98 | pointerEvents: 'none', 99 | }, 100 | willChange: 'width, height', 101 | })) 102 | 103 | const HeightUI = styled('div')` 104 | position: absolute; 105 | top: 0; 106 | left: 2px; 107 | height: 100%; 108 | display: flex; 109 | min-height: 0; 110 | align-items: center; 111 | justify-content: center; 112 | ` 113 | 114 | const HeightTextUI = styled('div')` 115 | transform: rotate(-90deg); 116 | will-change: contents; 117 | ` 118 | 119 | const WidthUI = styled('div')` 120 | position: absolute; 121 | top: 2px; 122 | width: 100%; 123 | text-align: center; 124 | will-change: contents; 125 | ` 126 | 127 | const connectedGuide = props => ( 128 | 129 | 130 | 131 | ) 132 | 133 | export default connectedGuide 134 | -------------------------------------------------------------------------------- /src/Guide/Guide.utils.ts: -------------------------------------------------------------------------------- 1 | export const defaultProps = { 2 | color: '#ec5381', 3 | opacity: 0.2, 4 | height: '100%', 5 | width: 25, 6 | position: 'absolute', 7 | showValues: true, 8 | showGuide: true, 9 | } 10 | -------------------------------------------------------------------------------- /src/Guide/__tests__/Guide.test.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react' 2 | import {mount} from 'enzyme' 3 | import Guide from '../index' 4 | import GuideProvider from '../../GuideProvider/index' 5 | import {dotcx} from '../../utils/index' 6 | import {findOne, getStyle} from '../../testHelpers' 7 | 8 | describe('Width/Height', () => { 9 | test('Renders with the specified width/height', () => { 10 | const wrapper = mount() 11 | const guideNode = findOne(wrapper, dotcx('Guide')) 12 | const widthNode = findOne(wrapper, dotcx('Guide__width')) 13 | const heightNode = findOne(wrapper, dotcx('Guide__height')) 14 | 15 | expect(widthNode.text()).toContain('123') 16 | expect(heightNode.text()).toContain('456') 17 | 18 | expect(getStyle(guideNode).width).toContain('123') 19 | expect(getStyle(guideNode).height).toContain('456') 20 | }) 21 | }) 22 | 23 | describe('Styles', () => { 24 | test('Renders CSS-based props as CSS styles', () => { 25 | const wrapper = mount() 26 | const guideNode = findOne(wrapper, dotcx('Guide')) 27 | 28 | expect(getStyle(guideNode).padding).toContain('100') 29 | expect(getStyle(guideNode).display).toContain('inline-flex') 30 | }) 31 | }) 32 | 33 | describe('showValues', () => { 34 | test('Values can be hidden', () => { 35 | const wrapper = mount() 36 | const guideNode = findOne(wrapper, dotcx('Guide')) 37 | const widthNode = findOne(wrapper, dotcx('Guide__width')) 38 | const heightNode = findOne(wrapper, dotcx('Guide__height')) 39 | 40 | expect(widthNode.length).toBe(0) 41 | expect(heightNode.length).toBe(0) 42 | 43 | expect(getStyle(guideNode).width).toContain('123') 44 | expect(getStyle(guideNode).height).toContain('456') 45 | }) 46 | }) 47 | 48 | describe('showGuide', () => { 49 | test('Does not render guide, if disabled', () => { 50 | const wrapper = mount() 51 | const guideNode = findOne(wrapper, dotcx('Guide')) 52 | const widthNode = findOne(wrapper, dotcx('Guide__width')) 53 | const heightNode = findOne(wrapper, dotcx('Guide__height')) 54 | 55 | expect(guideNode.length).toBe(0) 56 | expect(widthNode.length).toBe(0) 57 | expect(heightNode.length).toBe(0) 58 | }) 59 | }) 60 | 61 | describe('Provider', () => { 62 | test('Gets values from Provider', () => { 63 | const wrapper = mount( 64 | 65 |
66 | 67 |
68 |
, 69 | ) 70 | const guideNode = findOne(wrapper, dotcx('Guide')) 71 | const widthNode = findOne(wrapper, dotcx('Guide__width')) 72 | const heightNode = findOne(wrapper, dotcx('Guide__height')) 73 | 74 | expect(widthNode.length).toBe(0) 75 | expect(heightNode.length).toBe(0) 76 | 77 | expect(getStyle(guideNode).width).toContain('123') 78 | expect(getStyle(guideNode).height).toContain('456') 79 | }) 80 | }) 81 | 82 | describe('Unmount', () => { 83 | test('Unmounts without issues', () => { 84 | const wrapper = mount() 85 | wrapper.unmount() 86 | 87 | const guideNode = findOne(wrapper, dotcx('Guide')) 88 | 89 | expect(guideNode.length).toBe(0) 90 | }) 91 | }) 92 | -------------------------------------------------------------------------------- /src/Guide/index.ts: -------------------------------------------------------------------------------- 1 | import Guide from './Guide' 2 | 3 | export default Guide 4 | -------------------------------------------------------------------------------- /src/GuideContainer/GuideContainer.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react' 2 | import styled from '@helpscout/fancy' 3 | import classNames from '@helpscout/react-utils/dist/classNames' 4 | import {cx, getPreparedProps} from '../utils' 5 | 6 | class GuideContainer extends React.PureComponent { 7 | static defaultProps = { 8 | position: 'relative', 9 | width: '100%', 10 | height: '100%', 11 | zIndex: 1000, 12 | } 13 | 14 | render() { 15 | const {className, children, ...rest} = this.props 16 | return ( 17 | 22 | ) 23 | } 24 | } 25 | 26 | const GuideContainerUI = styled('div')(({children, ...props}) => ({ 27 | ...props, 28 | })) 29 | 30 | export default GuideContainer 31 | -------------------------------------------------------------------------------- /src/GuideContainer/__tests__/GuideContainer.test.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react' 2 | import {mount} from 'enzyme' 3 | import GuideContainer from '../index' 4 | import Guide from '../../Guide/index' 5 | import {dotcx} from '../../utils/index' 6 | import {findOne, getStyle} from '../../testHelpers' 7 | 8 | describe('Styles', () => { 9 | test('Renders CSS-based props as CSS styles', () => { 10 | const wrapper = mount( 11 | , 12 | ) 13 | const node = findOne(wrapper, dotcx('GuideContainer')) 14 | 15 | expect(getStyle(node).padding).toContain('100') 16 | }) 17 | }) 18 | 19 | describe('Guide', () => { 20 | test('Can render Guides', () => { 21 | const wrapper = mount( 22 | 23 | 24 | 25 | 26 | , 27 | ) 28 | const nodes = wrapper.find('Guide') 29 | 30 | expect(nodes.length).toBeGreaterThanOrEqual(3) 31 | }) 32 | }) 33 | -------------------------------------------------------------------------------- /src/GuideContainer/index.ts: -------------------------------------------------------------------------------- 1 | import GuideContainer from './GuideContainer' 2 | 3 | export default GuideContainer 4 | -------------------------------------------------------------------------------- /src/GuideContext/GuideContext.ts: -------------------------------------------------------------------------------- 1 | import createContext from '@helpscout/react-utils/dist/createContext' 2 | 3 | const Context = createContext({}) 4 | 5 | export default Context 6 | -------------------------------------------------------------------------------- /src/GuideContext/index.ts: -------------------------------------------------------------------------------- 1 | import GuideContext from './GuideContext' 2 | 3 | export default GuideContext 4 | -------------------------------------------------------------------------------- /src/GuideProvider/GuideProvider.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react' 2 | import GuideContext from '../GuideContext' 3 | 4 | class GuideProvider extends React.PureComponent { 5 | render() { 6 | const {children, ...rest} = this.props 7 | 8 | return ( 9 | 10 | {contextProps => { 11 | const mergedProps = { 12 | ...contextProps, 13 | ...rest, 14 | } 15 | 16 | return ( 17 | 18 | {children} 19 | 20 | ) 21 | }} 22 | 23 | ) 24 | } 25 | } 26 | 27 | export default GuideProvider 28 | -------------------------------------------------------------------------------- /src/GuideProvider/index.ts: -------------------------------------------------------------------------------- 1 | import GuideProvider from './GuideProvider' 2 | 3 | export default GuideProvider 4 | -------------------------------------------------------------------------------- /src/Resizer/Resizer.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react' 2 | import {ResizableBox} from 'react-resizable' 3 | import styled from '@helpscout/fancy' 4 | import {cx} from '../utils' 5 | 6 | export class Resizer extends React.PureComponent { 7 | static defaultProps = { 8 | height: 400, 9 | width: 400, 10 | withResponsiveHeight: false, 11 | withResponsiveWidth: false, 12 | } 13 | 14 | getResizableProps = () => { 15 | const { 16 | defaultHeight, 17 | defaultWidth, 18 | children, 19 | minWidth, 20 | minHeight, 21 | maxWidth, 22 | maxHeight, 23 | width: widthProp, 24 | height: heightProp, 25 | withResponsiveHeight, 26 | withResponsiveWidth, 27 | ...rest 28 | } = this.props 29 | 30 | const minHeightConstraint = minHeight ? minHeight : 1 31 | const minWidthConstraint = minWidth ? minWidth : 1 32 | 33 | const maxHeightConstraint = maxHeight ? maxHeight : Infinity 34 | const maxWidthConstraint = maxWidth ? maxWidth : Infinity 35 | 36 | const minConstraints = [minWidthConstraint, minHeightConstraint] 37 | const maxConstraints = [maxWidthConstraint, maxHeightConstraint] 38 | 39 | const width = widthProp || minWidth || defaultWidth 40 | const height = heightProp || minHeight || defaultHeight 41 | 42 | return { 43 | ...rest, 44 | minConstraints, 45 | maxConstraints, 46 | height, 47 | width, 48 | } 49 | } 50 | 51 | render() { 52 | return ( 53 | 54 | 55 | 56 | {this.props.children} 57 | 58 | 59 | 60 | ) 61 | } 62 | } 63 | 64 | const ResizerUI = styled('div')` 65 | .react-resizable { 66 | position: relative; 67 | will-change: height, width; 68 | } 69 | .react-resizable-handle { 70 | bottom: -25px; 71 | box-sizing: border-box; 72 | cursor: se-resize; 73 | height: 50px; 74 | position: absolute; 75 | right: -25px; 76 | width: 50px; 77 | 78 | &:before { 79 | content: ''; 80 | height: 12px; 81 | width: 12px; 82 | border-right: 2px solid rgba(0, 0, 0, 0.2); 83 | border-bottom: 2px solid rgba(0, 0, 0, 0.2); 84 | position: absolute; 85 | bottom: 15px; 86 | right: 15px; 87 | 88 | ${({theme}) => 89 | theme.darkMode && 90 | ` 91 | border-right: 2px solid rgba(255, 255, 255, 0.2); 92 | border-bottom: 2px solid rgba(255, 255, 255, 0.2); 93 | `}; 94 | } 95 | } 96 | ` 97 | 98 | const ContentUI = styled('div')` 99 | position: relative; 100 | 101 | ${({withResponsiveHeight}) => 102 | withResponsiveHeight && 103 | ` 104 | height: 100%; 105 | `}; 106 | 107 | ${({withResponsiveWidth}) => 108 | withResponsiveWidth && 109 | ` 110 | width: 100%; 111 | `}; 112 | ` 113 | 114 | export default Resizer 115 | -------------------------------------------------------------------------------- /src/Resizer/__tests__/Resizer.test.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react' 2 | import {mount} from 'enzyme' 3 | import Resizer from '../index' 4 | 5 | describe('Render', () => { 6 | test('Can render component', () => { 7 | expect(mount()).toBeTruthy() 8 | }) 9 | }) 10 | -------------------------------------------------------------------------------- /src/Resizer/index.ts: -------------------------------------------------------------------------------- 1 | import Resizer from './Resizer' 2 | 3 | export default Resizer 4 | -------------------------------------------------------------------------------- /src/SizeInspector/SizeInspector.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react' 2 | 3 | export interface Props { 4 | color: string 5 | offsetColor: string 6 | showOutlines: boolean 7 | targetSelector: string 8 | zoomLevel: number 9 | } 10 | 11 | const SIZE_NODE_CLASSNAME = 'SizeInspector__SizeNode' 12 | const SIZE_NODE_SELECTOR = `.${SIZE_NODE_CLASSNAME}` 13 | 14 | export class SizeInspector extends React.PureComponent { 15 | static defaultProps = { 16 | color: 'fuchsia', 17 | offsetColor: 'orange', 18 | targetSelector: '*', 19 | showOutlines: true, 20 | zoomLevel: 1, 21 | } 22 | 23 | node: HTMLDivElement 24 | currentNode?: HTMLElement 25 | sizeNode?: HTMLElement 26 | 27 | componentDidMount() { 28 | this.bindEvents() 29 | } 30 | 31 | componentWillUnmount() { 32 | this.unbindEvents() 33 | this.cleanUp() 34 | } 35 | 36 | componentDidUpdate() { 37 | this.unbindEvents() 38 | this.bindEvents() 39 | if (!this.props.showOutlines) { 40 | this.cleanUp() 41 | } 42 | } 43 | 44 | bindEvents = () => { 45 | if (!this.node) return 46 | if (!this.props.showOutlines) return 47 | 48 | Array.from(this.node.querySelectorAll(this.props.targetSelector)).forEach( 49 | node => { 50 | node.addEventListener('mouseenter', this.handleOnMouseEnter) 51 | node.addEventListener('mouseleave', this.handleOnMouseLeave) 52 | }, 53 | ) 54 | } 55 | 56 | unbindEvents = () => { 57 | if (!this.node) return 58 | 59 | Array.from(this.node.querySelectorAll(this.props.targetSelector)).forEach( 60 | node => { 61 | node.removeEventListener('mouseenter', this.handleOnMouseEnter) 62 | node.removeEventListener('mouseleave', this.handleOnMouseLeave) 63 | }, 64 | ) 65 | } 66 | 67 | cleanUp = () => { 68 | Array.from(document.querySelectorAll(SIZE_NODE_SELECTOR)).forEach( 69 | node => node && node.parentNode && node.parentNode.removeChild(node), 70 | ) 71 | } 72 | 73 | handleOnMouseEnter = event => { 74 | this.showSizeNode(event) 75 | } 76 | 77 | handleOnMouseLeave = event => { 78 | const node = event.target 79 | this.removeSizeNode() 80 | this.currentNode = undefined 81 | if (node.parentNode) { 82 | if (node.parentNode === this.node) { 83 | this.removeSizeNode() 84 | } else { 85 | this.showSizeNode({target: node.parentNode}) 86 | } 87 | } 88 | } 89 | 90 | showSizeNode = event => { 91 | if (!this.props.showOutlines) return 92 | const node = event.target 93 | this.addSizeNode(event) 94 | this.currentNode = node 95 | } 96 | 97 | addSizeNode = event => { 98 | this.removeSizeNode() 99 | const sizeNode = this.createSizeNode(event.target) 100 | document.body.appendChild(sizeNode) 101 | this.sizeNode = sizeNode 102 | } 103 | 104 | removeSizeNode = () => { 105 | if (this.sizeNode && this.sizeNode.parentNode) { 106 | this.sizeNode.parentNode.removeChild(this.sizeNode) 107 | } 108 | } 109 | 110 | createSizeNode = (targetNode: HTMLElement): HTMLElement => { 111 | const {color, offsetColor, zoomLevel} = this.props 112 | const sizeNode = document.createElement('div') 113 | const parentNode = targetNode.offsetParent 114 | const rect = targetNode.getBoundingClientRect() 115 | // @ts-ignore 116 | const parentRect = parentNode.getBoundingClientRect() 117 | sizeNode.classList.add(SIZE_NODE_CLASSNAME) 118 | 119 | setNodeStyles(sizeNode, { 120 | background: 'rgba(255, 255, 255, 0.7)', 121 | color, 122 | outline: '1px solid currentColor', 123 | fontFamily: 124 | '"SFMono-Regular",Consolas,"Liberation Mono",Menlo,Courier,monospace', 125 | fontSize: '10px', 126 | lineHeight: '1', 127 | height: `${rect.height}px`, 128 | width: `${rect.width}px`, 129 | position: 'fixed', 130 | top: `${rect.top}px`, 131 | left: `${rect.left}px`, 132 | zIndex: '999', 133 | pointerEvents: 'none', 134 | }) 135 | 136 | const zoomOffsetTop = Math.round(targetNode.offsetTop * zoomLevel) 137 | const zoomOffsetLeft = Math.round(targetNode.offsetLeft * zoomLevel) 138 | 139 | let offsetRight = 140 | // @ts-ignore 141 | parentRect.width - (zoomOffsetLeft + rect.width) 142 | offsetRight = offsetRight >= 0 ? offsetRight : 0 143 | 144 | let offsetBottom = 145 | // @ts-ignore 146 | parentRect.height - (zoomOffsetTop + rect.height) 147 | offsetBottom = offsetBottom >= 0 ? offsetBottom : 0 148 | 149 | // @ts-ignore 150 | const values = { 151 | width: Math.round(rect.width / zoomLevel), 152 | height: Math.round(rect.height / zoomLevel), 153 | offsetTop: Math.round(zoomOffsetTop / zoomLevel), 154 | offsetLeft: Math.round(zoomOffsetLeft / zoomLevel), 155 | offsetRight: Math.round(offsetRight / zoomLevel), 156 | offsetBottom: Math.round(offsetBottom / zoomLevel), 157 | } 158 | 159 | const widthNode = document.createElement('div') 160 | const heightNode = document.createElement('div') 161 | const heightTextNode = document.createElement('div') 162 | const topDistanceNode = document.createElement('div') 163 | const leftDistanceNode = document.createElement('div') 164 | const rightDistanceNode = document.createElement('div') 165 | const bottomDistanceNode = document.createElement('div') 166 | const distanceTopTextNode = document.createElement('div') 167 | const distanceLeftTextNode = document.createElement('div') 168 | const distanceRightTextNode = document.createElement('div') 169 | const distanceBottomTextNode = document.createElement('div') 170 | 171 | const alignCenterStyles = { 172 | boxSizing: 'border-box', 173 | display: 'flex', 174 | alignItems: 'center', 175 | justifyContent: 'center', 176 | textAlign: 'center', 177 | } 178 | 179 | setNodeStyles(widthNode, { 180 | boxSizing: 'border-box', 181 | color: 'currentColor', 182 | position: 'absolute', 183 | left: '50%', 184 | lineHeight: 'inherit', 185 | top: '1px', 186 | width: '20px', 187 | textAlign: 'center', 188 | marginLeft: '-10px', 189 | }) 190 | 191 | setNodeStyles(heightNode, { 192 | ...alignCenterStyles, 193 | color: 'currentColor', 194 | position: 'absolute', 195 | top: '0', 196 | lineHeight: 'inherit', 197 | height: '100%', 198 | left: '0px', 199 | display: 'flex', 200 | }) 201 | 202 | setNodeStyles(heightTextNode, { 203 | boxSizing: 'border-box', 204 | transform: 'rotate(-90deg)', 205 | }) 206 | 207 | setNodeStyles(topDistanceNode, { 208 | ...alignCenterStyles, 209 | color: offsetColor, 210 | backgroundColor: 'currentColor', 211 | width: '1px', 212 | height: `${values.offsetTop * zoomLevel}px`, 213 | position: 'absolute', 214 | left: '50%', 215 | top: `-${values.offsetTop * zoomLevel}px`, 216 | }) 217 | 218 | setNodeStyles(leftDistanceNode, { 219 | boxSizing: 'border-box', 220 | color: offsetColor, 221 | backgroundColor: 'currentColor', 222 | height: '1px', 223 | width: `${values.offsetLeft * zoomLevel}px`, 224 | position: 'absolute', 225 | top: '50%', 226 | left: `-${values.offsetLeft * zoomLevel}px`, 227 | }) 228 | 229 | setNodeStyles(rightDistanceNode, { 230 | boxSizing: 'border-box', 231 | color: offsetColor, 232 | backgroundColor: 'currentColor', 233 | height: '1px', 234 | width: `${values.offsetRight * zoomLevel}px`, 235 | position: 'absolute', 236 | top: '50%', 237 | right: `-${values.offsetRight * zoomLevel}px`, 238 | }) 239 | 240 | setNodeStyles(bottomDistanceNode, { 241 | ...alignCenterStyles, 242 | color: offsetColor, 243 | backgroundColor: 'currentColor', 244 | width: '1px', 245 | height: `${values.offsetBottom * zoomLevel}px`, 246 | position: 'absolute', 247 | left: '50%', 248 | bottom: `-${values.offsetBottom * zoomLevel}px`, 249 | }) 250 | 251 | setNodeStyles(distanceTopTextNode, { 252 | boxSizing: 'border-box', 253 | transform: 'translateX(-100%)', 254 | marginRight: '-2px', 255 | }) 256 | 257 | setNodeStyles(distanceLeftTextNode, { 258 | boxSizing: 'border-box', 259 | transform: 'translateY(-100%)', 260 | width: '100%', 261 | paddingRight: '3px', 262 | textAlign: 'right', 263 | }) 264 | 265 | setNodeStyles(distanceRightTextNode, { 266 | boxSizing: 'border-box', 267 | transform: 'translateY(-100%)', 268 | width: '100%', 269 | paddingLeft: '3px', 270 | textAlign: 'left', 271 | }) 272 | 273 | setNodeStyles(distanceBottomTextNode, { 274 | boxSizing: 'border-box', 275 | transform: 'translateX(-100%)', 276 | marginRight: '-2px', 277 | }) 278 | 279 | widthNode.innerHTML = `${values.width}` 280 | heightTextNode.innerHTML = `${values.height}` 281 | distanceTopTextNode.innerHTML = `${values.offsetTop}` 282 | distanceLeftTextNode.innerHTML = `${values.offsetLeft}` 283 | distanceRightTextNode.innerHTML = `${values.offsetRight}` 284 | distanceBottomTextNode.innerHTML = `${values.offsetBottom}` 285 | 286 | heightNode.appendChild(heightTextNode) 287 | topDistanceNode.appendChild(distanceTopTextNode) 288 | leftDistanceNode.appendChild(distanceLeftTextNode) 289 | rightDistanceNode.appendChild(distanceRightTextNode) 290 | bottomDistanceNode.appendChild(distanceBottomTextNode) 291 | 292 | if (values.offsetTop > 0) { 293 | sizeNode.appendChild(topDistanceNode) 294 | } 295 | if (values.offsetLeft > 0) { 296 | sizeNode.appendChild(leftDistanceNode) 297 | } 298 | if (values.offsetRight > 0) { 299 | sizeNode.appendChild(rightDistanceNode) 300 | } 301 | if (values.offsetBottom > 0) { 302 | sizeNode.appendChild(bottomDistanceNode) 303 | } 304 | sizeNode.appendChild(heightNode) 305 | sizeNode.appendChild(widthNode) 306 | 307 | return sizeNode 308 | } 309 | 310 | setNodeRef = node => (this.node = node) 311 | 312 | render() { 313 | const {children} = this.props 314 | 315 | return
316 | } 317 | } 318 | 319 | export function setNodeStyles(node: HTMLElement, styles: Object): HTMLElement { 320 | Object.keys(styles).forEach(k => { 321 | node.style[k] = styles[k] 322 | }) 323 | 324 | return node 325 | } 326 | 327 | export default SizeInspector 328 | -------------------------------------------------------------------------------- /src/SizeInspector/__tests__/SizeInspector.test.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react' 2 | import {mount} from 'enzyme' 3 | import SizeInspector from '../index' 4 | 5 | describe('Render', () => { 6 | test('Can render component', () => { 7 | expect(mount()).toBeTruthy() 8 | }) 9 | }) 10 | -------------------------------------------------------------------------------- /src/SizeInspector/index.ts: -------------------------------------------------------------------------------- 1 | import SizeInspector from './SizeInspector' 2 | 3 | export default SizeInspector 4 | -------------------------------------------------------------------------------- /src/UI/Base/Base.tsx: -------------------------------------------------------------------------------- 1 | import styled from '@helpscout/fancy' 2 | 3 | const BaseUI = styled('div')` 4 | box-sizing: border-box; 5 | font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Helvetica, Arial, 6 | sans-serif, 'Apple Color Emoji', 'Segoe UI Emoji', 'Segoe UI Symbol'; 7 | font-size: 11px; 8 | ` 9 | 10 | export default BaseUI 11 | -------------------------------------------------------------------------------- /src/UI/Base/__tests__/Base.test.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react' 2 | import {mount} from 'enzyme' 3 | import Base from '../Base' 4 | import {getStyle} from '../../../testHelpers' 5 | 6 | describe('Base', () => { 7 | test('Renders a div with default styles', () => { 8 | const wrapper = mount() 9 | const el = wrapper.find('div') 10 | 11 | expect(el.length).toBe(1) 12 | expect(getStyle(el, 'box-sizing')).toBe('border-box') 13 | expect(getStyle(el, 'font-family')).toContain('apple-system') 14 | }) 15 | }) 16 | 17 | describe('Content', () => { 18 | test('Renders content', () => { 19 | const wrapper = mount(Hallo) 20 | 21 | expect(wrapper.text()).toContain('Hallo') 22 | }) 23 | }) 24 | -------------------------------------------------------------------------------- /src/UI/Base/index.ts: -------------------------------------------------------------------------------- 1 | import Base from './Base' 2 | 3 | export default Base 4 | -------------------------------------------------------------------------------- /src/UI/Button/Button.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react' 2 | import styled from '@helpscout/fancy' 3 | import {darken, lighten} from 'polished' 4 | import Icon from '../Icon' 5 | import {noop} from '../../utils' 6 | 7 | export class Button extends React.PureComponent { 8 | static defaultProps = { 9 | isActive: false, 10 | icon: undefined, 11 | onClick: noop, 12 | } 13 | 14 | renderContent = () => { 15 | const {children, icon} = this.props 16 | 17 | if (icon && Icon[icon]) { 18 | const IconComponent = Icon[icon] 19 | return 20 | } 21 | 22 | return children 23 | } 24 | 25 | render() { 26 | return {this.renderContent()} 27 | } 28 | } 29 | 30 | const ButtonUI = styled('button')` 31 | font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Helvetica, Arial, 32 | sans-serif, 'Apple Color Emoji', 'Segoe UI Emoji', 'Segoe UI Symbol'; 33 | font-size: 11px; 34 | 35 | appearance: none; 36 | display: inline-block; 37 | padding: 3px 8px; 38 | margin-bottom: 0; 39 | line-height: 1.4; 40 | white-space: nowrap; 41 | background-image: none; 42 | border: 1px solid #0000; 43 | border-radius: 4px; 44 | box-shadow: 0 1px 1px rgba(0, 0, 0, 0.06); 45 | box-sizing: border-box; 46 | color: #333; 47 | background-color: #fcfcfc; 48 | background-image: linear-gradient(to bottom, #fcfcfc 0, #f1f1f1 100%); 49 | border-color: #c2c0c2 #c2c0c2 #a19fa1; 50 | outline: none; 51 | 52 | &:active { 53 | background-color: #ddd; 54 | background-image: none; 55 | } 56 | 57 | ${({theme}) => 58 | theme.darkMode && 59 | ` 60 | background-color: #232223; 61 | background-image: none; 62 | border-color: #000; 63 | color: white; 64 | 65 | &:active { 66 | background-color: #2b2a2b; 67 | } 68 | `}; 69 | 70 | ${({isActive}) => 71 | isActive && 72 | ` 73 | background-color: #3c93f7; 74 | background-image: linear-gradient(to bottom, ${lighten( 75 | 0.05, 76 | '#3c93f7', 77 | )} 0, #3c93f7 100%); 78 | background-image: none; 79 | border-color: #0b78f5 #0b78f5 ${darken(0.05, '#0b78f5')}; 80 | color: white; 81 | 82 | &:active { 83 | background-color: ${darken(0.05, '#3c93f7')}; 84 | color: white; 85 | } 86 | `}; 87 | ` 88 | 89 | export default Button 90 | -------------------------------------------------------------------------------- /src/UI/Button/__tests__/Button.test.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react' 2 | import {mount} from 'enzyme' 3 | import Button from '../Button' 4 | import {getStyle} from '../../../testHelpers' 5 | 6 | describe('Children', () => { 7 | test('Can render content', () => { 8 | const wrapper = mount() 9 | 10 | expect(wrapper.text()).toBe('Hallo') 11 | }) 12 | }) 13 | 14 | describe('Icon', () => { 15 | test('Does not render an icon by default', () => { 16 | const wrapper = mount( 60 |
61 | 62 | )) 63 | 64 | stories.add('zoomLevel', () => ( 65 | 66 |
74 | 75 | 76 |
77 |
78 | )) 79 | 80 | stories.add('snapshots', () => ( 81 | 87 |
95 | 96 | 97 |
98 |
99 | )) 100 | 101 | stories.add('Guides (Components)', () => ( 102 | , 106 | , 107 | , 108 | , 109 | ]} 110 | id="local-artboard-guides" 111 | > 112 |
113 | 116 |
117 |
118 | )) 119 | 120 | stories.add('Guides (Objects)', () => ( 121 | 128 |
129 | 132 |
133 |
134 | )) 135 | 136 | stories.add('Guides (Component + Objects)', () => ( 137 | , 141 | , 142 | {height: '100%', left: 15, width: 10}, 143 | {height: '100%', right: 15, width: 10}, 144 | ]} 145 | > 146 |
147 | 150 |
151 |
152 | )) 153 | 154 | stories.add('Without center guides', () => ( 155 | , 161 | , 162 | , 163 | , 164 | ]} 165 | > 166 |
167 | 170 |
171 |
172 | )) 173 | 174 | stories.add('Dark Mode', () => ( 175 | , 182 | , 183 | , 184 | , 185 | ]} 186 | > 187 |
188 | 191 |
192 |
193 | )) 194 | 195 | stories.add('Size', () => ( 196 | 203 |
204 |

Artboard

205 |
206 |
207 | )) 208 | -------------------------------------------------------------------------------- /stories/ArtboardProvider.stories.js: -------------------------------------------------------------------------------- 1 | import * as React from 'react' 2 | import {storiesOf} from '@storybook/react' 3 | import Button from '@helpscout/hsds-react/components/Button' 4 | import Artboard from '../src/Artboard' 5 | import ArtboardProvider from '../src/ArtboardProvider' 6 | import Guide from '../src/Guide' 7 | 8 | const stories = storiesOf('ArtboardProvider', module) 9 | 10 | stories.add('Dark Mode Test', () => ( 11 | 12 | , 15 | , 16 | , 17 | , 18 | ]} 19 | > 20 | 23 | 24 | 25 | )) 26 | -------------------------------------------------------------------------------- /stories/Blue.stories.js: -------------------------------------------------------------------------------- 1 | import * as React from 'react' 2 | import {storiesOf} from '@storybook/react' 3 | import OptionTile, {OptionTileGuides} from './Examples/OptionTile.Example' 4 | 5 | const stories = storiesOf('OptionTile', module) 6 | 7 | stories.add('OptionTile', () => ( 8 |
19 | 20 | 21 |
22 | )) 23 | -------------------------------------------------------------------------------- /stories/BoxInspector.stories.js: -------------------------------------------------------------------------------- 1 | import * as React from 'react' 2 | import {storiesOf} from '@storybook/react' 3 | import OptionTile, {OptionTileGuides} from './Examples/OptionTile.Example' 4 | import BoxInspector from '../src/BoxInspector' 5 | 6 | const stories = storiesOf('BoxInspector', module) 7 | 8 | stories.add('Example', () => ( 9 |
10 | Hover your mouse on any element 11 |
12 |
23 | 24 | 25 | 26 | 27 |
28 |
29 | )) 30 | -------------------------------------------------------------------------------- /stories/Crosshair.stories.js: -------------------------------------------------------------------------------- 1 | import * as React from 'react' 2 | import {storiesOf} from '@storybook/react' 3 | import Crosshair from '../src/Crosshair' 4 | 5 | const stories = storiesOf('Crosshair', module) 6 | 7 | stories.add('Example', () => { 8 | class Example extends React.Component { 9 | state = { 10 | isActive: false, 11 | } 12 | 13 | toggle = () => { 14 | this.setState({ 15 | isActive: !this.state.isActive, 16 | }) 17 | } 18 | 19 | render() { 20 | return ( 21 |
22 | 23 | 24 |
25 | ) 26 | } 27 | } 28 | 29 | return 30 | }) 31 | -------------------------------------------------------------------------------- /stories/Examples/OptionTile.Example.js: -------------------------------------------------------------------------------- 1 | import * as React from 'react' 2 | import OptionTile from '@helpscout/hsds-react/components/OptionTile' 3 | import GuideContainer from '../../src/GuideContainer' 4 | import Guide from '../../src/Guide' 5 | 6 | export const OptionTileGuides = () => ( 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 22 | 23 | ) 24 | 25 | export default () => ( 26 | 27 | 33 | 39 | 40 | ) 41 | -------------------------------------------------------------------------------- /stories/Eyedropper.stories.js: -------------------------------------------------------------------------------- 1 | import * as React from 'react' 2 | import {storiesOf} from '@storybook/react' 3 | import Eyedropper from '../src/Eyedropper' 4 | import image from '../images/colors.jpg' 5 | 6 | const stories = storiesOf('Eyedropper', module) 7 | 8 | stories.add('Example', () => { 9 | return ( 10 |
11 |

Click to start!

12 |

Click to start!

13 |

Click to start!

14 |

Click to start!

15 | 16 | 17 |
18 | ) 19 | }) 20 | -------------------------------------------------------------------------------- /stories/Grid.stories.js: -------------------------------------------------------------------------------- 1 | import * as React from 'react' 2 | import {storiesOf} from '@storybook/react' 3 | import Grid from '../src/Grid' 4 | import Resizer from '../src/Resizer' 5 | 6 | const stories = storiesOf('Grid', module) 7 | 8 | stories.add('Example', () => { 9 | const props = { 10 | height: '100%', 11 | } 12 | 13 | return ( 14 | 15 | 16 | 17 | ) 18 | }) 19 | -------------------------------------------------------------------------------- /stories/Guide.stories.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import {storiesOf} from '@storybook/react' 3 | import GuideProvider from '../src/GuideProvider' 4 | import GuideContainer from '../src/GuideContainer' 5 | import Guide from '../src/Guide' 6 | 7 | const stories = storiesOf('Basic', module) 8 | 9 | stories.add('Example', () => { 10 | const guideProps = { 11 | position: 'relative', 12 | margin: '0 10px', 13 | width: '80px', 14 | } 15 | 16 | return ( 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | ) 30 | }) 31 | -------------------------------------------------------------------------------- /stories/Resizer.stories.js: -------------------------------------------------------------------------------- 1 | import * as React from 'react' 2 | import {storiesOf} from '@storybook/react' 3 | import OptionTile, {OptionTileGuides} from './Examples/OptionTile.Example' 4 | import Resizer from '../src/Resizer' 5 | 6 | const stories = storiesOf('Resizer', module) 7 | 8 | stories.add('Example', () => ( 9 | 10 |
20 | 21 | 22 |
23 |
24 | )) 25 | -------------------------------------------------------------------------------- /stories/SizeInspector.stories.js: -------------------------------------------------------------------------------- 1 | import * as React from 'react' 2 | import {storiesOf} from '@storybook/react' 3 | import Artboard from '../src/Artboard' 4 | 5 | const stories = storiesOf('SizeInspector', module) 6 | 7 | stories.add('Example', () => ( 8 | 9 |
10 | 11 |
12 |
13 | )) 14 | -------------------------------------------------------------------------------- /stories/Storybook.stories.js: -------------------------------------------------------------------------------- 1 | import * as React from 'react' 2 | import {storiesOf} from '@storybook/react' 3 | import {withKnobs, text, boolean, number} from '@storybook/addon-knobs' 4 | import Artboard from '../src/Artboard' 5 | 6 | const stories = storiesOf('Storybook', module) 7 | 8 | stories.addDecorator(storyFn => ( 9 | {storyFn()} 10 | )) 11 | stories.addDecorator(withKnobs) 12 | 13 | // Knobs for React props 14 | stories.add('with a button', () => ( 15 | 18 | )) 19 | 20 | // Knobs as dynamic variables. 21 | stories.add('as dynamic variables', () => { 22 | const name = text('Name', 'Arunoda Susiripala') 23 | const age = number('Age', 89) 24 | 25 | const content = `I am ${name} and I'm ${age} years old.` 26 | return
{content}
27 | }) 28 | -------------------------------------------------------------------------------- /stories/UI.Button.stories.js: -------------------------------------------------------------------------------- 1 | import * as React from 'react' 2 | import {storiesOf} from '@storybook/react' 3 | import Artboard from '../src/Artboard' 4 | import Button from '../src/UI/Button' 5 | 6 | const stories = storiesOf('UI/Button', module) 7 | 8 | stories.add('Basic', () => ( 9 | 10 | 11 | 12 | 13 | )) 14 | 15 | stories.add('Icon', () => ( 16 | 17 | 18 | 21 | 22 | )) 23 | -------------------------------------------------------------------------------- /stories/UI.ButtonControl.stories.js: -------------------------------------------------------------------------------- 1 | import * as React from 'react' 2 | import {storiesOf} from '@storybook/react' 3 | import Artboard from '../src/Artboard' 4 | import ButtonControl from '../src/UI/ButtonControl' 5 | 6 | const stories = storiesOf('UI/ButtonControl', module) 7 | 8 | stories.add('Basic', () => ( 9 | 10 | ButtonControl 11 | Active 12 | 13 | )) 14 | 15 | stories.add('Icon', () => ( 16 | 17 | ButtonControl 18 | 19 | ButtonControl 20 | 21 | 22 | )) 23 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "allowJs": false, 4 | "allowSyntheticDefaultImports": true, 5 | "baseUrl": "src", 6 | "declaration": true, 7 | "experimentalDecorators": true, 8 | "forceConsistentCasingInFileNames": true, 9 | "jsx": "react", 10 | "lib": ["es5", "es6", "es7", "es2017", "dom"], 11 | "module": "commonjs", 12 | "moduleResolution": "node", 13 | "noImplicitAny": false, 14 | "noImplicitReturns": true, 15 | "noImplicitThis": true, 16 | "noUnusedLocals": true, 17 | "outDir": "dist", 18 | "rootDirs": ["src", "stories"], 19 | "sourceMap": true, 20 | "strictNullChecks": true, 21 | "suppressImplicitAnyIndexErrors": true, 22 | "target": "es5" 23 | }, 24 | "include": ["src/**/*"], 25 | "exclude": ["node_modules"] 26 | } 27 | --------------------------------------------------------------------------------