├── .eslintignore ├── .eslintrc ├── .flowconfig ├── .gitignore ├── .npmignore ├── .vscode └── settings.json ├── CHANGELOG.md ├── README.md ├── babel.config.js ├── examples └── sketch │ ├── package-lock.json │ ├── package.json │ ├── src │ ├── components │ │ ├── AppBar.jsx │ │ ├── BarChart.jsx │ │ ├── InputField.jsx │ │ ├── Select.jsx │ │ └── TextInput.jsx │ ├── index.js │ ├── manifest.json │ ├── screens │ │ ├── Home.jsx │ │ ├── Signup.jsx │ │ └── Visual.jsx │ └── styles │ │ └── theme.js │ ├── static-blog.sketchplugin │ └── Contents │ │ └── Sketch │ │ ├── index.js │ │ ├── index.js.map │ │ └── manifest.json │ └── webpack.skpm.config.js ├── index.js ├── index.native.js ├── package-lock.json ├── package.json ├── rollup.config.js ├── sketch.js ├── src ├── @types │ ├── react-primitives-svg │ │ └── index.d.ts │ ├── react-primitives │ │ └── index.d.ts │ └── styled-system │ │ └── index.d.ts ├── LayoutProvider.tsx ├── ThemeProvider.tsx ├── atoms │ ├── Box │ │ ├── Box.tsx │ │ └── index.ts │ ├── Button │ │ ├── Button.tsx │ │ └── index.ts │ ├── Circle │ │ ├── Circle.tsx │ │ └── index.ts │ ├── Image │ │ ├── Image.tsx │ │ └── index.ts │ ├── Line │ │ ├── Line.tsx │ │ └── index.ts │ ├── Rectangle │ │ ├── Rectangle.tsx │ │ └── index.ts │ ├── Text │ │ ├── Text.tsx │ │ └── index.ts │ └── index.ts ├── context.ts ├── hooks │ ├── index.ts │ ├── use-color-scheme │ │ ├── index.figma.ts │ │ ├── index.native.ts │ │ ├── index.sketch.ts │ │ └── index.web.ts │ ├── use-dimensions │ │ ├── index.figma.ts │ │ ├── index.native.ts │ │ ├── index.sketch.ts │ │ └── index.web.ts │ ├── use-hover.js │ ├── use-style-state.tsx │ └── use-viewport │ │ └── index.web.ts ├── index.ts ├── molecules │ ├── Form │ │ ├── Input.tsx │ │ ├── TextInput │ │ │ ├── TextInput.figma.tsx │ │ │ ├── TextInput.native.tsx │ │ │ ├── TextInput.sketch.tsx │ │ │ ├── TextInput.web.tsx │ │ │ └── index.ts │ │ └── index.ts │ ├── Row │ │ ├── Row.tsx │ │ └── index.ts │ ├── Typography │ │ ├── Headline.tsx │ │ └── index.ts │ └── index.ts ├── styled.ts └── utils │ ├── extend.tsx │ ├── index.ts │ ├── shadow.ts │ └── styles.ts ├── tsconfig.json └── tsconfig.module.json /.eslintignore: -------------------------------------------------------------------------------- 1 | dist/ 2 | -------------------------------------------------------------------------------- /.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "extends": [ 3 | "airbnb", 4 | "plugin:flowtype/recommended" 5 | ], 6 | "plugins": [ 7 | "flowtype" 8 | ], 9 | "rules": { 10 | "import/prefer-default-export": 0, 11 | "import/no-named-as-default": 0, 12 | "flowtype/semi": "error", 13 | "react/default-props-match-prop-types": 0, 14 | "flowtype/delimiter-dangle": [ 15 | 2, 16 | "always-multiline" 17 | ] 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /.flowconfig: -------------------------------------------------------------------------------- 1 | [libs] 2 | ./node_modules/fbjs/flow/lib 3 | 4 | [options] 5 | esproposal.class_static_fields=enable 6 | esproposal.class_instance_fields=enable 7 | 8 | module.name_mapper='^\(.*\)\.css$' -> 'react-scripts/config/flow/css' 9 | module.name_mapper='^\(.*\)\.\(jpg\|png\|gif\|eot\|svg\|ttf\|woff\|woff2\|mp4\|webm\)$' -> 'react-scripts/config/flow/file' 10 | 11 | suppress_type=$FlowIssue 12 | suppress_comment= \\(.\\|\n\\)*\\$FlowFixMe 13 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | yarn-debug.log* 6 | yarn-error.log* 7 | 8 | # Runtime data 9 | pids 10 | *.pid 11 | *.seed 12 | *.pid.lock 13 | 14 | # Directory for instrumented libs generated by jscoverage/JSCover 15 | lib-cov 16 | 17 | # Coverage directory used by tools like istanbul 18 | coverage 19 | 20 | # nyc test coverage 21 | .nyc_output 22 | 23 | # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) 24 | .grunt 25 | 26 | # Bower dependency directory (https://bower.io/) 27 | bower_components 28 | 29 | # node-waf configuration 30 | .lock-wscript 31 | 32 | # Compiled binary addons (https://nodejs.org/api/addons.html) 33 | build/Release 34 | 35 | # Dependency directories 36 | node_modules/ 37 | jspm_packages/ 38 | 39 | # TypeScript v1 declaration files 40 | typings/ 41 | 42 | # Optional npm cache directory 43 | .npm 44 | 45 | # Optional eslint cache 46 | .eslintcache 47 | 48 | # Optional REPL history 49 | .node_repl_history 50 | 51 | # Output of 'npm pack' 52 | *.tgz 53 | 54 | # Yarn Integrity file 55 | .yarn-integrity 56 | 57 | # dotenv environment variables file 58 | .env 59 | 60 | # next.js build output 61 | .next 62 | 63 | .DS_Store 64 | 65 | # TODO: fix up examples 66 | examples 67 | 68 | lib/ 69 | -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | *.log* 3 | node_modules 4 | _book 5 | examples 6 | docs 7 | .vscode 8 | yarn.lock 9 | -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | {} 2 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | All notable changes to this project will be documented in this file. 4 | 5 | ## Unreleased Changes 6 | 7 | - 8 | - 9 | - 10 | 11 | ## Versions 12 | 13 | ## 0.3.3 14 | 15 | - Create web umd/esm build 16 | 17 | 18 | ## 0.3.2 19 | 20 | - Fix 21 | 22 | ## 0.3.1 23 | 24 | - Forward ref for 25 | - Pass `src` prop for `` on web 26 | - Concat `px` to `` `lineHeight` prop transformation 27 | 28 | ## 0.3.0 29 | 30 | - Add React Native support 31 | - Add `useWindowDimensions` hook (cross-platform primitive) 32 | 33 | ### 0.2.0 34 | 35 | - Add cross-platform shadow resolving support (`boxShadow` interface) 36 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Elemental React 2 | 3 | > Build UI components once, render to any platform using `react-primitives`. This library abstracts away common UI patterns for you. 4 | 5 | [![npm](https://img.shields.io/npm/v/elemental-react.svg)](https://www.npmjs.com/package/elemental-react) 6 | [![npm](https://img.shields.io/npm/dt/elemental-react.svg)](https://www.npmjs.com/package/elemental-react) 7 | 8 | 9 | Abstraction for app presentation to speed up cross-platform UI design and development with code using React/Sketch as a design function. This is an underlying cross-platform abstraction wrapper that allows you to build your own design language 10 | 11 | > Based off [`styled-system`]() and [`styled-components`](). API is similar to [`rebass`](https://github.com/rebassjs/rebass), but using React Native style components. 12 | 13 | This is an **alpha/preview** release. Please **test** comprehensively before using in **production**. 14 | 15 | **Supported React Renderers:** 16 | 17 | - `react` - React web 18 | - `react-native` - React Native (WIP) 19 | - `react-sketchapp` - React Sketch.app 20 | - **more** - Post an issue to suggest more! Ideally an API should exist that lets you override the primitives 21 | 22 | ## Getting Started 23 | 24 | ```sh 25 | npm install elemental-react 26 | ``` 27 | 28 | ```jsx 29 | import React from 'react'; 30 | import { 31 | Box, Text, Button, 32 | } from 'elemental-react'; 33 | 34 | // ... 35 | return ( 36 | 37 | 38 | Hello World 39 | 40 | 41 | ); 42 | ``` 43 | 44 | ## Example UI 45 | 46 | Quick example of a design created by a coder (me :slightly_smiling_face:), developed with live rendering to `react-sketchapp`: 47 | ![Example Blog UI](https://user-images.githubusercontent.com/6757532/63878429-7e849500-c9c1-11e9-915f-33bd0e82a3be.png) 48 | 49 | ## Related Reading 50 | 51 | - https://daneden.me/2018/01/05/subatomic-design-systems/ 52 | - https://medium.com/styled-components/build-better-component-libraries-with-styled-system-4951653d54ee 53 | - https://medium.com/@_alanbsmith/layered-components-6f18996073a8 54 | - https://medium.com/@_alanbsmith/component-api-design-3ff378458511 55 | 56 | ## Design Properties 57 | 58 | ### Line 59 | Themed colour (primary) 60 | - Weight 61 | - Color 62 | - Texture 63 | - Style 64 | 65 | 66 | ### Shape 67 | Foundational element. 68 | - Depth 69 | - Light, shadow and depth (illusion) 70 | 71 | ### Texture 72 | Physical quality of a surface. 73 | 74 | ### Balance 75 | Equal distribution of visual weight – spacing. 76 | - Symmetry (each side is the same) 77 | - Asymmetry – evenly distribute weight 78 | - Rule of thirds – grid divided into thirds 79 | 80 | 81 | ### Color 82 | 83 | **Properties** 84 | - Hue 85 | - Saturation 86 | - Monochromatic 87 | - Value 88 | 89 | **Analagous Colour Scheme** 90 | -------------------------------------------------------------------------------- /babel.config.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable quote-props, quotes, comma-dangle */ 2 | 3 | module.exports = { 4 | "presets": [ 5 | "@babel/preset-env", 6 | "@babel/preset-react", 7 | "@babel/preset-typescript" 8 | ], 9 | "plugins": [ 10 | "@babel/plugin-proposal-class-properties" 11 | // "babel-plugin-styled-components" 12 | ] 13 | }; 14 | -------------------------------------------------------------------------------- /examples/sketch/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "static-blog", 3 | "version": "0.0.1", 4 | "description": "", 5 | "skpm": { 6 | "main": "static-blog.sketchplugin", 7 | "manifest": "src/manifest.json" 8 | }, 9 | "scripts": { 10 | "build": "skpm-build", 11 | "watch": "skpm-build --watch", 12 | "render": "skpm-build --watch --run", 13 | "render:once": "skpm-build --run", 14 | "postinstall": "npm run build && skpm-link" 15 | }, 16 | "author": "Macintosh Helper ", 17 | "license": "MIT", 18 | "devDependencies": { 19 | "@skpm/builder": "^0.5.15" 20 | }, 21 | "dependencies": { 22 | "@vx/gradient": "0.0.183", 23 | "@vx/group": "0.0.183", 24 | "@vx/mock-data": "0.0.185", 25 | "@vx/scale": "0.0.182", 26 | "@vx/shape": "0.0.184", 27 | "chroma-js": "^1.2.2", 28 | "d3-scale": "^3.0.0", 29 | "elemental-react": "^0.1.1", 30 | "prop-types": "^15.5.8", 31 | "react": "^16.13.1", 32 | "react-primitives": "^0.8.1", 33 | "react-primitives-svg": "0.0.3", 34 | "react-sketchapp": "^3.1.1", 35 | "react-test-renderer": "^16.13.1", 36 | "styled-components": "^5.1.0", 37 | "styled-system": "^5.1.5", 38 | "yoga-layout-prebuilt": "^1.9.5" 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /examples/sketch/src/components/AppBar.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { Box, Line, Button } from 'elemental-react'; 3 | 4 | 5 | 6 | export const MenuIcon = () => ( 7 | 8 | 9 | 10 | 11 | 12 | ); 13 | 14 | export const ActionButton = ({ children, ...props }) => ( 15 | 18 | ) 19 | 20 | const AppBar = ({ children, ...props }) => ( 21 | 22 | {children} 23 | 24 | ); 25 | 26 | AppBar.MenuIcon = MenuIcon; 27 | AppBar.ActionButton = ActionButton; 28 | 29 | export default AppBar; 30 | -------------------------------------------------------------------------------- /examples/sketch/src/components/BarChart.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { Bar } from '@vx/shape'; 3 | import { Group } from '@vx/group'; 4 | import { GradientTealBlue } from '@vx/gradient'; 5 | import { letterFrequency } from '@vx/mock-data'; 6 | import { scaleBand, scaleLinear } from '@vx/scale'; 7 | 8 | import { Svg, Rect } from 'react-primitives-svg'; 9 | 10 | 11 | const data = letterFrequency.slice(5); 12 | 13 | 14 | // accessors 15 | const x = d => d.letter; 16 | const y = d => +d.frequency * 100; 17 | 18 | export default ({ width, height }) => { 19 | // bounds 20 | const xMax = width; 21 | const yMax = height - 120; 22 | 23 | // scales 24 | const xScale = scaleBand({ 25 | rangeRound: [0, xMax], 26 | domain: data.map(x), 27 | padding: 0.4 28 | }); 29 | const yScale = scaleLinear({ 30 | rangeRound: [yMax, 0], 31 | domain: [0, Math.max(...data.map(y))] 32 | }); 33 | 34 | return ( 35 | 36 | 37 | 38 | 39 | {data.map((d, i) => { 40 | const letter = x(d); 41 | const barWidth = xScale.bandwidth(); 42 | const barHeight = yMax - yScale(y(d)); 43 | const barX = xScale(letter); 44 | const barY = yMax - barHeight; 45 | return ( 46 | 54 | ); 55 | })} 56 | 57 | 58 | ); 59 | }; 60 | -------------------------------------------------------------------------------- /examples/sketch/src/components/InputField.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { Box, Text } from 'elemental-react'; 3 | 4 | const InputField = ({ label, placeholder, error, value = '', children, ...props }) => ( 5 | 6 | 0 ? 1 : 0}> 7 | 8 | {label && value.length > 0 ? label : ''} 9 | 10 | 11 | 12 | {children({ label, placeholder, error, value })} 13 | 14 | 15 | {error && ( 16 | 17 | {error} 18 | 19 | )} 20 | 21 | 22 | ); 23 | export default InputField; 24 | -------------------------------------------------------------------------------- /examples/sketch/src/components/Select.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { Box, Line, Text } from 'elemental-react'; 3 | 4 | const Select = ({ label, placeholder, error, value = '', ...props }) => ( 5 | 6 | 7 | 8 | {value || placeholder || label} 9 | 10 | 11 | 12 | 13 | 14 | ); 15 | export default Select; 16 | -------------------------------------------------------------------------------- /examples/sketch/src/components/TextInput.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { Line, TextInput } from 'elemental-react'; 3 | 4 | const InputField = ({ label, placeholder, error, value = '', children, ...props }) => ( 5 | <> 6 | 7 | 8 | 9 | ); 10 | export default InputField; 11 | -------------------------------------------------------------------------------- /examples/sketch/src/index.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable react/jsx-filename-extension */ 2 | /* globals context */ 3 | import React, { Component } from 'react'; 4 | import * as PropTypes from 'prop-types'; 5 | import { 6 | width as styledWidth, position, space, height as styledHeight, 7 | } from 'styled-system'; 8 | import { 9 | render, Document, Page, Artboard, RedBox, 10 | } from 'react-sketchapp'; 11 | import chroma from 'chroma-js'; 12 | 13 | import { 14 | styled, ThemeProvider, Box, Text, 15 | } from 'elemental-react'; 16 | 17 | import theme from './styles/theme'; 18 | 19 | import HomeScreen from './screens/Home'; 20 | import SignupScreen from './screens/Signup'; 21 | 22 | const StyledText = styled(Text)` 23 | color: blue; 24 | `; 25 | 26 | const Screen = styled(Artboard)` 27 | ${styledWidth} 28 | ${position} 29 | ${space} 30 | ${styledHeight} 31 | `; 32 | 33 | Screen.defaultProps = { 34 | width: 360, 35 | position: 'relative', 36 | ml: 0, 37 | }; 38 | 39 | class ErrorBoundary extends Component { 40 | constructor(props) { 41 | super(props); 42 | this.state = { hasError: false }; 43 | } 44 | 45 | // static getDerivedStateFromError(error) { 46 | // // Update state so the next render will show the fallback UI. 47 | // return { hasError: true, error }; 48 | // } 49 | 50 | componentDidCatch(error, info) { 51 | this.setState({ 52 | error, 53 | hasError: true, 54 | }); 55 | // You can also log the error to an error reporting service 56 | console.log(` 57 | ${JSON.stringify(error, null, 2)}\n 58 | ${JSON.stringify(info, null, 2)} 59 | `); 60 | } 61 | 62 | render() { 63 | // eslint-disable-next-line react/prop-types 64 | const { children } = this.props; 65 | const { error, hasError } = this.state; 66 | 67 | if (hasError) { 68 | // You can render any custom fallback UI 69 | return ; 70 | } 71 | 72 | return children; 73 | } 74 | } 75 | 76 | // take a hex and give us a nice text color to put over it 77 | const textColor = (hex) => { 78 | const vsWhite = chroma.contrast(hex, 'white'); 79 | if (vsWhite > 4) { 80 | return '#FFF'; 81 | } 82 | return chroma(hex) 83 | .darken(3) 84 | .hex(); 85 | }; 86 | 87 | const SwatchTile = styled.View` 88 | height: 250px; 89 | width: 250px; 90 | border-radius: 125px; 91 | margin: 4px; 92 | background-color: ${props => props.hex}; 93 | justify-content: center; 94 | align-items: center; 95 | `; 96 | 97 | const SwatchName = styled.Text` 98 | color: ${props => textColor(props.hex)}; 99 | font-size: 32px; 100 | font-weight: bold; 101 | `; 102 | 103 | const Ampersand = styled.Text` 104 | color: ${props => textColor(props.hex)}; 105 | font-size: 120px; 106 | font-family: Himalaya; 107 | line-height: 144px; 108 | `; 109 | 110 | const Swatch = ({ name, hex }) => ( 111 | 112 | 113 | & 114 | 115 | 116 | 117 | {name} 118 | 119 | 120 | {hex} 121 | 122 | 123 | 124 | ); 125 | 126 | const Color = { 127 | hex: PropTypes.string.isRequired, 128 | name: PropTypes.string.isRequired, 129 | }; 130 | 131 | Swatch.propTypes = Color; 132 | 133 | const screens = [{ 134 | name: 'Android', width: 360, height: 640, 135 | }, { 136 | name: 'Tablet', width: 1024, height: 768, 137 | }, { 138 | name: 'Desktop', width: 1280, height: 720, 139 | }]; 140 | 141 | const routes = [{ 142 | name: 'Home', 143 | component: HomeScreen, 144 | }, { 145 | name: 'Signup', 146 | component: SignupScreen, 147 | }]; 148 | 149 | const DocumentContainer = ({ colors }) => ( 150 | 151 | 152 | 153 | <> 154 | 155 | {routes.map(({ name: routeName, component: Comp }) => ( 156 | 157 | {screens.map(({ name, height, width }) => ( 158 | 159 | 160 | 161 | ))} 162 | 163 | ))} 164 | 165 | 166 | 167 | 168 | Colours 169 | {Object.keys(colors).map(color => ( 170 | typeof colors[color] === 'string' ? ( 171 | 172 | ) : ( 173 | 174 | {colors[color].map((shade, i) => ( 175 | 176 | ))} 177 | 178 | ) 179 | ))} 180 | 181 | 182 | 183 | 184 | 185 | 186 | 187 | ); 188 | 189 | Document.propTypes = { 190 | colors: PropTypes.objectOf(PropTypes.string).isRequired, 191 | }; 192 | 193 | export default () => { 194 | const data = context.document.documentData(); 195 | const pages = context.document.pages(); 196 | render(); 197 | data.setCurrentPage(pages.firstObject()); 198 | }; 199 | -------------------------------------------------------------------------------- /examples/sketch/src/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "compatibleVersion": 3, 3 | "bundleVersion": 1, 4 | "commands": [ 5 | { 6 | "name": "static-blog: Blog", 7 | "identifier": "main", 8 | "script": "./index.js" 9 | } 10 | ], 11 | "menu": { 12 | "isRoot": true, 13 | "items": [ 14 | "main" 15 | ] 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /examples/sketch/src/screens/Home.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { Box, Text, Button, Headline } from 'elemental-react'; 3 | 4 | import AppBar from '../components/AppBar'; 5 | import InputField from '../components/InputField'; 6 | import TextInput from '../components/TextInput'; 7 | 8 | const Home = () => ( 9 | <> 10 | 11 | 12 | 13 | 14 | SIGN IN 15 | 16 | 17 | 18 | 19 | The Blog 20 | 21 | 22 | We solve problems 23 | 24 | 25 | 26 | 27 | {({ label, value }) => } 28 | 29 | 34 | 35 | 36 | 37 | 38 | RECENT POSTS 39 | 40 | {[{ 41 | title: 'An important announcement', 42 | category: 'News', 43 | description: 'Some important information about the incorporation of this blog.', 44 | }, { 45 | title: 'Looking back', 46 | category: 'History', 47 | description: 'While checking our archives, we found some interesting data.', 48 | }, { 49 | title: 'You won\'t believe what we found', 50 | category: 'Trends', 51 | description: 'Clickbait at its finest. Please don\'t click through as this is a dummy site and it won\'t work', 52 | }].map(({ title, category, description }) => ( 53 | 54 | {category} 55 | {title} 56 | {description} 57 | 58 | Written by John Smith 59 | 60 | 61 | ))} 62 | 63 | 64 | ); 65 | 66 | export default Home; 67 | -------------------------------------------------------------------------------- /examples/sketch/src/screens/Signup.jsx: -------------------------------------------------------------------------------- 1 | import React, { Fragment } from 'react'; 2 | import { Rectangle, Box, Circle, Line, Text, Image, Button, Headline } from 'elemental-react'; 3 | 4 | import AppBar from '../components/AppBar'; 5 | import InputField from '../components/InputField'; 6 | import TextInput from '../components/TextInput'; 7 | import Select from '../components/Select'; 8 | 9 | 10 | const Home = () => ( 11 | 12 | 13 | 14 | 15 | 16 | The Blog 17 | 18 | 19 | 20 | 21 | 22 | 23 | {({ label, value }) => } 24 | 25 | 26 | {({ label, value }) => } 27 | 28 | 29 | {({ label, value }) => ( 30 | 31 | 33 |