Repository: styled-system/styled-system
Pull request #490 (2019-05-14 17:25:00)

Bring in "extra" packages into monorepo
WIP: separate out individual packages: 
- [x] Confirm naming of each
- [x] Confirm what goes into each
- [x] Move experimental css and/or JSX pragma packages into monorepo
- [x] Move should-forward-prop package into monorepo

Part of #475 

jxnblk: Sorry, but I think I've made updates to at least the `css` package since you opened this. Since #484 just got merged in, maybe it would make sense to move some (not all) of the packages in `extras` over one-by-one. I'll go ahead and move `css` over, but if you want to pull some of these changes into a separate PR for the `should-forward-prop` package, that could be cool. Then, maybe we can start to tackle #475 into a v5 working branch – let me know if that makes sense to you
mfix22: @jxnblk that makes perfect sense to me! Feel free to push directly to this branch. I'll get the latest from the packages I've already merged in updated today. 

As for #475, that was exactly what I was thinking. Open up a `v5` branch and we can start making PRs against that. 
mfix22: @jxnblk which other packages from https://github.com/styled-system/extras/ do you want merged in here? 
mfix22: @jxnblk don't mean to overload the comments here, but I also have some of the breaking changes work from #475 completed on a local branch. Once we get this PR merged, let's open up a `v5` WIP branch and I'll create PRs against that one. 
jxnblk: @mfix22 I've started doing some refactoring on the core part of styled-system, so I wouldn't go too far with splitting up the current v4 code in the `styled-system` package. As for the packages in styled-system/extras go, I think `should-forward-prop`, `jsx`, and maybe `edit` are good candidates for moving into the monorepo. I'll go ahead and start a v5 branch to work towards, but the packages from the extras repo could be merged into master as-is
mfix22: Oh I see! I'll update this branch to pull in new packages, but I won't split up `styled-system` yet.
mfix22: @jxnblk any other packages to pull in here? 

As for the rest of V5, I don't want to step on your toes, but is there anything else I can pick up? 
jxnblk: This looks great, thanks! I say we go ahead and merge this one into master and keep tackling the v5 work in new PRs

Patch:
diff --git a/babel.config.js b/babel.config.js
index b98087b..a7dfe86 100644
--- a/babel.config.js
+++ b/babel.config.js
@@ -1,29 +1,13 @@
 module.exports = {
+  presets: [['@babel/env', { loose: true }], '@babel/preset-react'],
+  plugins: ['@babel/transform-runtime'],
   env: {
-    test: {
-      presets: [
-        [ '@babel/env', { loose: true } ],
-        '@babel/react'
-      ],
-      plugins: [
-        '@babel/transform-runtime'
-      ]
-    },
-    cjs: {
-      presets: [
-        [ '@babel/env', { loose: true } ]
-      ],
-      plugins: [
-        '@babel/transform-runtime'
-      ]
-    },
     esm: {
       presets: [
-        [ '@babel/env', { loose: true, modules: false } ]
+        ['@babel/env', { loose: true, modules: false }],
+        '@babel/preset-react',
       ],
-      plugins: [
-        ['@babel/transform-runtime', { useESModules: true }]
-      ]
+      plugins: [['@babel/transform-runtime', { useESModules: true }]],
     },
-  }
+  },
 }
diff --git a/lerna.json b/lerna.json
index 7ae4412..be320af 100644
--- a/lerna.json
+++ b/lerna.json
@@ -1,6 +1,12 @@
 {
   "version": "independent",
-  "packages": [
-    "packages/*"
-  ]
+  "packages": ["packages/*"],
+  "create": {
+    "license": "MIT"
+  },
+  "command": {
+    "bootstrap": {
+      "npmClientArgs": ["--no-package-lock"]
+    }
+  }
 }
diff --git a/packages/css/src/index.js b/packages/css/src/index.js
index 578955e..5ca42d5 100644
--- a/packages/css/src/index.js
+++ b/packages/css/src/index.js
@@ -10,12 +10,8 @@ export const get = (obj, key = '', def, p, undef) => {
 const defaultBreakpoints = [40, 52, 64].map(n => n + 'em')
 
 const defaultTheme = {
-  space: [
-    0, 4, 8, 16, 32, 64, 128, 256, 512
-  ],
-  fontSizes: [
-    12, 14, 16, 20, 24, 32, 48, 64, 72
-  ],
+  space: [0, 4, 8, 16, 32, 64, 128, 256, 512],
+  fontSizes: [12, 14, 16, 20, 24, 32, 48, 64, 72],
 }
 
 const aliases = {
@@ -37,10 +33,10 @@ const aliases = {
 }
 
 const directions = {
-  marginX: [ 'marginLeft', 'marginRight' ],
-  marginY: [ 'marginTop', 'marginBottom' ],
-  paddingX: [ 'paddingLeft', 'paddingRight' ],
-  paddingY: [ 'paddingTop', 'paddingBottom' ],
+  marginX: ['marginLeft', 'marginRight'],
+  marginY: ['marginTop', 'marginBottom'],
+  paddingX: ['paddingLeft', 'paddingRight'],
+  paddingY: ['paddingTop', 'paddingBottom'],
 }
 
 const scales = {
diff --git a/packages/edit/.gitignore b/packages/edit/.gitignore
new file mode 100644
index 0000000..3dd4a08
--- /dev/null
+++ b/packages/edit/.gitignore
@@ -0,0 +1,1 @@
+dist.js
diff --git a/packages/edit/.npmignore b/packages/edit/.npmignore
new file mode 100644
index 0000000..d8f8d46
--- /dev/null
+++ b/packages/edit/.npmignore
@@ -0,0 +1,1 @@
+docs
diff --git a/packages/edit/README.md b/packages/edit/README.md
new file mode 100644
index 0000000..7a35e82
--- /dev/null
+++ b/packages/edit/README.md
@@ -0,0 +1,87 @@
+
+# @styled-system/edit
+
+**WIP** Debugging tool for live editing Styled System theme objects
+
+*Currently only works with Emotion*
+
+```sh
+npm i @styled-system/edit
+```
+
+```jsx
+import React from 'react'
+import { ThemeProvider } from 'emotion-theming'
+import { EditProvider, ThemeControls } from '@styled-system/edit'
+import theme from './theme'
+
+export default props =>
+  <ThemeProvider theme={theme}>
+    <EditProvider>
+      {props.children}
+      <ThemeControls />
+    </EditProvider>
+  </ThemeProvider>
+```
+
+## Components
+
+- `EditProvider` a stateful theme provider with context for live editing – receives `theme` from parent context
+- `ThemeControls` a full, batteries-included theme editing form
+- `FieldSet` renders fields for part of a `theme` object
+- `Field` renders a form field for a single value from the `theme` object
+
+```jsx
+// example using FieldSet
+import React from 'react'
+import { ThemeProvider } from 'emotion-theming'
+import { EditProvider, ThemeControls } from '@styled-system/edit'
+import theme from './theme'
+
+export default props =>
+  <ThemeProvider theme={theme}>
+    <EditProvider>
+      {props.children}
+      <FieldSet name='colors' />
+      <FieldSet name='fontSizes' />
+    </EditProvider>
+  </ThemeProvider>
+```
+
+```jsx
+// example using Field
+import React from 'react'
+import { ThemeProvider } from 'emotion-theming'
+import { EditProvider, ThemeControls } from '@styled-system/edit'
+import theme from './theme'
+
+export default props =>
+  <ThemeProvider theme={theme}>
+    <EditProvider>
+      {props.children}
+      <FieldSet name='colors.text' />
+      <FieldSet name='colors.background' />
+    </EditProvider>
+  </ThemeProvider>
+```
+
+## `FieldSet`
+
+**Props**
+
+- `name` (string) dot-notation key path for `theme` object
+- `type` (string) type prop for child Field components (see below)
+- `ignore` (array) array of key names to omit from the form
+
+## `Field`
+
+**Props**
+
+- `name` (string) dot-notation key path for `theme` object
+- `type` (string | 'number', 'select', or 'color') type of form field to render
+- `options` (array) array of options to render for `select` type fields
+- `render` (function) render prop for custom field UI
+
+MIT License
+
+
diff --git a/packages/edit/docs/index.js b/packages/edit/docs/index.js
new file mode 100644
index 0000000..7c078ce
--- /dev/null
+++ b/packages/edit/docs/index.js
@@ -0,0 +1,58 @@
+import React from 'react'
+import { EditProvider, ThemeControls, Field, ResetButton } from '../index'
+import { ThemeProvider } from 'emotion-theming'
+import css from '@styled-system/css'
+import Readme from '../README.md'
+
+const theme = {
+  colors: {
+    text: '#000',
+    background: '#fff',
+    primary: '#07c',
+    secondary: '#05a',
+    modes: {
+      dark: {
+        text: '#fff',
+        background: '#000',
+      },
+    },
+  },
+  fontSizes: [12, 14, 16, 20, 24, 32, 48, 64, 96],
+  lineHeights: {
+    body: 1.5,
+    heading: 1.25,
+  },
+  space: [0, 4, 8, 16, 32, 64, 128],
+}
+
+export default props => (
+  <ThemeProvider theme={theme}>
+    <EditProvider>
+      <div
+        css={css({
+          fontSize: 2,
+          lineHeight: 'body',
+          color: 'text',
+          bg: 'background',
+        })}
+      >
+        <h1
+          css={css({
+            fontSize: [5, 6],
+          })}
+        >
+          Hello
+        </h1>
+        <Field name="colors.text" type="color" />
+        <ResetButton>Reset</ResetButton>
+        <ThemeControls
+          css={{
+            float: 'right',
+            margin: 8,
+          }}
+        />
+        <Readme />
+      </div>
+    </EditProvider>
+  </ThemeProvider>
+)
diff --git a/packages/edit/index.js b/packages/edit/index.js
new file mode 100644
index 0000000..5dc8a77
--- /dev/null
+++ b/packages/edit/index.js
@@ -0,0 +1,240 @@
+/** @jsx jsx */
+import { jsx, ThemeContext } from '@emotion/core'
+import React, { useReducer, useContext } from 'react'
+import { ThemeProvider } from 'emotion-theming'
+import merge from 'lodash.merge'
+import omit from 'lodash.omit'
+import get from 'lodash.get'
+import set from 'lodash.set'
+import Color from 'color'
+
+export const EditContext = React.createContext({})
+
+// not actually a reducer
+const notAReducer = (state, next) =>
+  typeof next === 'function' ? next(state) : merge({}, state, next)
+
+export const EditProvider = ({
+  initialTheme,
+  ignore = ['styles'],
+  children,
+}) => {
+  const theme = useContext(ThemeContext) || initialTheme
+  const [state, setState] = useReducer(notAReducer, theme)
+  const context = {
+    ignore,
+    state,
+    setState,
+    reset: () => {
+      setState(theme)
+    },
+  }
+
+  return (
+    <EditContext.Provider value={context}>
+      <ThemeProvider theme={state}>{children}</ThemeProvider>
+    </EditContext.Provider>
+  )
+}
+
+// context-aware field
+const Input = props => (
+  <input
+    {...props}
+    css={{
+      width: '100%',
+      margin: 0,
+    }}
+  />
+)
+
+const Select = ({ options = [], ...props }) => (
+  <select
+    {...props}
+    css={{
+      width: '100%',
+      margin: 0,
+    }}
+  >
+    {options.map(option => (
+      <option key={option}>{option}</option>
+    ))}
+  </select>
+)
+
+const ColorInput = props => (
+  <>
+    <input
+      type="color"
+      {...props}
+      value={toHex(props.value)}
+      onChange={e => {
+        const next = merge({}, e, {
+          target: {
+            value: toHex(e.target.value),
+          },
+        })
+        props.onChange(next)
+      }}
+      css={{
+        flex: 'none',
+        minWidth: 0,
+        appearance: 'none',
+        margin: 0,
+        width: 32,
+        height: 24,
+        border: 0,
+        backgroundColor: 'transparent',
+      }}
+    />
+    <Input {...props} css={{}} />
+  </>
+)
+
+export const Field = ({ name, type, options, render, ...props }) => {
+  const context = useContext(EditContext)
+  const value = get(context.state, name)
+  const onChange = e => {
+    context.setState(set({}, name, e.target.value))
+  }
+
+  if (typeof render === 'function') {
+    return render({
+      name,
+      type,
+      options,
+      value,
+      onChange,
+      ...context,
+    })
+  }
+
+  const inputProps = {
+    name,
+    value,
+    onChange,
+    ...props,
+  }
+
+  let field = <Input {...inputProps} />
+
+  switch (type) {
+    case 'number':
+      field = <Input type="number" {...inputProps} />
+      break
+    case 'select':
+      field = <Select {...inputProps} options={options} />
+      break
+    case 'color':
+      field = <ColorInput {...inputProps} />
+      break
+  }
+
+  return (
+    <label
+      css={{
+        display: 'flex',
+        maxWidth: 512,
+      }}
+    >
+      <div
+        css={{
+          width: '65%',
+        }}
+      >
+        {name}
+      </div>
+      <div
+        css={{
+          display: 'flex',
+          width: '35%',
+        }}
+      >
+        {field}
+      </div>
+    </label>
+  )
+}
+
+const toHex = n => {
+  try {
+    return Color(n).hex()
+  } catch (e) {
+    return n
+  }
+}
+
+export const FieldSet = ({ name, type, ignore = [], ...props }) => {
+  const context = useContext(EditContext)
+  const value = get(context.state, name)
+
+  return (
+    <div>
+      <h3
+        css={{
+          marginTop: 0,
+          marginBottom: 0,
+        }}
+      >
+        {name}
+      </h3>
+      {Object.keys(omit(value, ignore)).map(key => {
+        const val = value[key]
+        if (val && typeof val === 'object') {
+          return (
+            <FieldSet
+              {...props}
+              name={`${name}.${key}`}
+              type={type}
+              ignore={ignore}
+            />
+          )
+        }
+        return <Field {...props} name={`${name}.${key}`} type={type} />
+      })}
+    </div>
+  )
+}
+
+const getType = (key, value) => {
+  switch (key) {
+    case 'colors':
+      return 'color'
+    case 'space':
+    case 'fontSizes':
+      return 'number'
+    default:
+      return
+  }
+}
+
+export const ThemeControls = ({ ignore, ...props }) => {
+  const context = useContext(EditContext)
+  const keys = Object.keys(omit(context.state, ignore || context.ignore))
+  return (
+    <div
+      {...props}
+      css={{
+        fontFamily: 'system-ui, sans-serif',
+        fontSize: 12,
+        lineHeight: 1.5,
+        color: '#000',
+        backgroundColor: '#eee',
+        maxWidth: 320,
+        maxHeight: '100vh',
+        padding: 8,
+        overflowY: 'auto',
+        WebkitOverflowScrolling: 'touch',
+      }}
+    >
+      {keys.map(key => (
+        <FieldSet key={key} {...context} name={key} type={getType(key)} />
+      ))}
+    </div>
+  )
+}
+
+export const ResetButton = props => {
+  const { reset } = useContext(EditContext)
+  return <button {...props} onClick={reset} />
+}
diff --git a/packages/edit/package.json b/packages/edit/package.json
new file mode 100644
index 0000000..cef7417
--- /dev/null
+++ b/packages/edit/package.json
@@ -0,0 +1,31 @@
+{
+  "name": "@styled-system/edit",
+  "version": "0.0.5",
+  "description": "",
+  "main": "dist.js",
+  "scripts": {
+    "prepare": "babel --root-mode upward index.js -o dist.js",
+    "docs": "gx docs"
+  },
+  "keywords": [],
+  "author": "Brent Jackson <jxnblk@gmail.com>",
+  "license": "MIT",
+  "devDependencies": {
+    "@babel/cli": "^7.4.3",
+    "@babel/core": "^7.4.3",
+    "@babel/preset-env": "^7.4.3",
+    "@babel/preset-react": "^7.0.0",
+    "@styled-system/css": "^1.0.3",
+    "gx": "^2.0.0-23",
+    "react": "^16.8.6"
+  },
+  "dependencies": {
+    "@emotion/core": "^10.0.10",
+    "color": "^3.1.0",
+    "emotion-theming": "^10.0.10",
+    "lodash.get": "^4.4.2",
+    "lodash.merge": "^4.6.1",
+    "lodash.omit": "^4.5.0",
+    "lodash.set": "^4.3.2"
+  }
+}
diff --git a/packages/jsx/.gitignore b/packages/jsx/.gitignore
new file mode 100644
index 0000000..b23cd7f
--- /dev/null
+++ b/packages/jsx/.gitignore
@@ -0,0 +1,1 @@
+es5.js
diff --git a/packages/jsx/.npmignore b/packages/jsx/.npmignore
new file mode 100644
index 0000000..da371e1
--- /dev/null
+++ b/packages/jsx/.npmignore
@@ -0,0 +1,3 @@
+test.js
+babel.config.js
+coverage
diff --git a/packages/jsx/README.md b/packages/jsx/README.md
new file mode 100644
index 0000000..be71054
--- /dev/null
+++ b/packages/jsx/README.md
@@ -0,0 +1,91 @@
+
+# @styled-system/jsx
+
+Custom JSX pragma for `css` prop with Styled System - built with [Emotion][]
+
+```jsx
+/** @jsx jsx */
+import jsx from '@styled-system/jsx'
+
+export default props =>
+  <div
+    {...props}
+    css={{
+      fontSize: 32,
+      // use theme values
+      color: 'primary',
+      // responsive arrays
+      padding: [ 2, 3, 4 ],
+      // use shorthands
+      mx: 'auto',
+    }}
+  />
+```
+
+- Use Styled System seamlessly with the `css` prop
+- No custom `styled` HOC API required
+- Mix with *any* CSS property
+- Pick up theme values for anything
+- Apply any CSS property responsively with array props
+
+## Getting Started
+
+If you're already using JSX syntax, the most explicit way to use Styled System JSX is by adding a custom pragma to the top of any module that uses the pragma.
+
+```js
+/** @jsx jsx */
+import jsx from '@styled-system/jsx'
+```
+
+This is similar to what the Emotion plugin does, so the Emotion plugin should be disabled when using this one instead.
+
+For any module with the custom pragma enabled, use Styled System shortcuts, responsive arrays and theme values directly in the `css` prop.
+
+```jsx
+<div
+  css={{
+    px: 3,
+    py: 4,
+    mb: 4,
+    bg: 'primary'
+  }}
+/>
+```
+
+To use explicit values instead of referencing values from the theme, use a value that will *not* be interpreted as an object key.
+For example, use `2px` instead of `2` (which would pick up the `space[2]` value.
+
+```jsx
+<div css={{ mb: '2px' }} />
+```
+
+## Styled Components
+
+To create reusable styled components, wrap an element like so.
+
+```jsx
+/** @jsx jsx */
+import jsx from '@styled-system/jsx'
+
+export const Button = ({
+  primary,
+  ...props
+}) =>
+  <button
+    {...props}
+    css={{
+      color: 'white',
+      bg: primary ? 'primary' : 'secondary',
+    }}
+  />
+```
+
+### Differences from core Styled System
+
+- No `width` fractions
+- No need to define custom style functions
+- No responsive object syntax (this would get messy with nested styles)
+
+[emotion]: https://emotion.sh
+
+MIT License
diff --git a/packages/jsx/index.js b/packages/jsx/index.js
new file mode 100644
index 0000000..608ae8c
--- /dev/null
+++ b/packages/jsx/index.js
@@ -0,0 +1,17 @@
+import { jsx as emotion } from '@emotion/core'
+import css from '@styled-system/css'
+
+export const jsx = (type, props, ...children) => {
+  return emotion.apply(undefined, [
+    type,
+    props
+      ? {
+          ...props,
+          css: css(props.css),
+        }
+      : null,
+    ...children,
+  ])
+}
+
+export default jsx
diff --git a/packages/jsx/package.json b/packages/jsx/package.json
new file mode 100644
index 0000000..aa1efd4
--- /dev/null
+++ b/packages/jsx/package.json
@@ -0,0 +1,28 @@
+{
+  "name": "@styled-system/jsx",
+  "version": "0.0.3",
+  "description": "",
+  "type": "module",
+  "main": "es5.js",
+  "module": "index.js",
+  "scripts": {
+    "prepare": "babel --root-mode upward index.js -o es5.js"
+  },
+  "author": "Brent Jackson <jxnblk@gmail.com>",
+  "license": "MIT",
+  "dependencies": {
+    "@emotion/core": "^10.0.10",
+    "@styled-system/css": "^2.0.0"
+  },
+  "devDependencies": {
+    "@babel/cli": "^7.4.4",
+    "@babel/core": "^7.4.4",
+    "@babel/preset-env": "^7.4.4",
+    "@babel/preset-react": "^7.0.0",
+    "@babel/register": "^7.4.4",
+    "jest": "^24.7.1",
+    "jest-emotion": "^10.0.10",
+    "react": "^16.8.6",
+    "react-test-renderer": "^16.8.6"
+  }
+}
diff --git a/packages/should-forward-prop/README.md b/packages/should-forward-prop/README.md
new file mode 100644
index 0000000..721c67d
--- /dev/null
+++ b/packages/should-forward-prop/README.md
@@ -0,0 +1,26 @@
+
+# @styled-system/should-forward-prop
+
+Utility for filtering Styled System props with Emotion's shouldForwardProp option
+
+```sh
+npm i @styled-system/should-forward-prop
+```
+
+```js
+import styled from '@emotion/styled'
+import {
+  space,
+  color,
+  fontSize
+} from 'styled-system'
+import shouldForwardProp from '@styled-system/should-forward-prop'
+
+const Box = styled('div', { shouldForwardProp })(
+  space,
+  color,
+  fontSize
+)
+```
+
+MIT License
\ No newline at end of file
diff --git a/packages/should-forward-prop/index.js b/packages/should-forward-prop/index.js
new file mode 100644
index 0000000..f2c4e2d
--- /dev/null
+++ b/packages/should-forward-prop/index.js
@@ -0,0 +1,7 @@
+const memoize = require('@emotion/memoize').default
+const isPropValid = require('@emotion/is-prop-valid').default
+const systemProps = require('styled-system/props')
+
+const regex = new RegExp(`^(${systemProps.join('|')})$`)
+
+module.exports = memoize(prop => isPropValid(prop) && !regex.test(prop))
diff --git a/packages/should-forward-prop/package.json b/packages/should-forward-prop/package.json
new file mode 100644
index 0000000..4fc6537
--- /dev/null
+++ b/packages/should-forward-prop/package.json
@@ -0,0 +1,14 @@
+{
+  "name": "@styled-system/should-forward-prop",
+  "version": "1.0.0",
+  "description": "Utility for filtering Styled System props with Emotion's shouldForwardProp option",
+  "main": "index.js",
+  "keywords": [],
+  "author": "Brent Jackson <jxnblk@gmail.com>",
+  "license": "MIT",
+  "dependencies": {
+    "@emotion/is-prop-valid": "^0.7.3",
+    "@emotion/memoize": "^0.7.1",
+    "styled-system": "^4.1.0"
+  }
+}


Test patch:
diff --git a/packages/css/test/index.js b/packages/css/test/index.js
index ee48684..bb1c192 100644
--- a/packages/css/test/index.js
+++ b/packages/css/test/index.js
@@ -5,9 +5,7 @@ const theme = {
     primary: 'tomato',
     secondary: 'cyan',
   },
-  fontSizes: [
-    12, 14, 16, 24, 36
-  ],
+  fontSizes: [12, 14, 16, 24, 36],
   fonts: {
     monospace: 'Menlo, monospace',
   },
@@ -28,15 +26,15 @@ const theme = {
   },
   text: {
     caps: {
-      fontSize: [ 1, 2 ],
+      fontSize: [1, 2],
       letterSpacing: '0.1em',
       textTransform: 'uppercase',
     },
     title: {
-      fontSize: [ 3, 4 ],
-      letterSpacing: [ '-0.01em', '-0.02em']
+      fontSize: [3, 4],
+      letterSpacing: ['-0.01em', '-0.02em'],
     },
-  }
+  },
 }
 
 test('returns a function', () => {
@@ -65,7 +63,7 @@ test('returns styles', () => {
 test('returns system props styles', () => {
   const result = css({
     color: 'primary',
-    fontSize: [ 2, 3, 4]
+    fontSize: [2, 3, 4],
   })({ theme })
   expect(result).toEqual({
     fontSize: 16,
@@ -84,13 +82,13 @@ test('returns nested system props styles', () => {
     color: 'primary',
     '&:hover': {
       color: 'secondary',
-    }
+    },
   })({ theme })
   expect(result).toEqual({
     color: 'tomato',
     '&:hover': {
       color: 'cyan',
-    }
+    },
   })
 })
 
@@ -99,7 +97,7 @@ test('returns nested responsive styles', () => {
     color: 'primary',
     h1: {
       py: [3, 4],
-    }
+    },
   })({ theme })
   expect(result).toEqual({
     color: 'tomato',
@@ -109,8 +107,8 @@ test('returns nested responsive styles', () => {
       '@media screen and (min-width: 40em)': {
         paddingTop: 32,
         paddingBottom: 32,
-      }
-    }
+      },
+    },
   })
 })
 
@@ -199,7 +197,7 @@ test('handles variants with responsive values', () => {
     textTransform: 'uppercase',
     '@media screen and (min-width: 40em)': {
       fontSize: 16,
-    }
+    },
   })
 })
 
@@ -213,7 +211,7 @@ test('handles responsive variants', () => {
     '@media screen and (min-width: 40em)': {
       fontSize: 36,
       letterSpacing: '-0.02em',
-    }
+    },
   })
 })
 
diff --git a/packages/jsx/test/index.js b/packages/jsx/test/index.js
new file mode 100644
index 0000000..c866913
--- /dev/null
+++ b/packages/jsx/test/index.js
@@ -0,0 +1,157 @@
+/** @jsx jsx */
+import jsx from '../index'
+import renderer from 'react-test-renderer'
+import serializer, { matchers } from 'jest-emotion'
+import { ThemeContext } from '@emotion/core'
+
+expect.addSnapshotSerializer(serializer)
+expect.extend(matchers)
+
+const renderJSON = el => renderer.create(el).toJSON()
+
+const theme = {
+  colors: {
+    primary: '#609',
+  },
+}
+
+test('renders with styles', () => {
+  const json = renderJSON(
+    <div
+      css={{
+        color: 'tomato',
+      }}
+    />
+  )
+  expect(json).toHaveStyleRule('color', 'tomato')
+})
+
+test('renders with responsive styles', () => {
+  const json = renderJSON(
+    <div
+      css={{
+        padding: [1, 2, 3],
+      }}
+    />
+  )
+  expect(json).toHaveStyleRule('padding', '4px')
+  expect(json).toHaveStyleRule('padding', '8px', {
+    media: 'screen and (min-width: 40em)',
+  })
+  expect(json).toHaveStyleRule('padding', '16px', {
+    media: 'screen and (min-width: 52em)',
+  })
+})
+
+test('renders with shorthand props', () => {
+  const json = renderJSON(
+    <div
+      css={{
+        m: 2,
+        p: 3,
+        bg: 'tomato',
+      }}
+    />
+  )
+  expect(json).toHaveStyleRule('margin', '8px')
+  expect(json).toHaveStyleRule('padding', '16px')
+  expect(json).toHaveStyleRule('background-color', 'tomato')
+})
+
+test('renders with theme values', () => {
+  const json = renderJSON(
+    <ThemeContext.Provider value={theme}>
+      <div
+        css={{
+          color: 'primary',
+        }}
+      />
+    </ThemeContext.Provider>
+  )
+  expect(json).toHaveStyleRule('color', '#609')
+})
+
+test('renders nested styles', () => {
+  const json = renderJSON(
+    <div
+      css={{
+        '&:hover': {
+          color: 'tomato',
+        },
+      }}
+    />
+  )
+  expect(json).toHaveStyleRule('color', 'tomato', {
+    target: ':hover',
+  })
+})
+
+test('renders bi-directional props', () => {
+  const json = renderJSON(
+    <div
+      css={{
+        px: 2,
+        py: 3,
+        my: 4,
+      }}
+    />
+  )
+  expect(json).toHaveStyleRule('padding-left', '8px')
+  expect(json).toHaveStyleRule('padding-right', '8px')
+  expect(json).toHaveStyleRule('padding-top', '16px')
+  expect(json).toHaveStyleRule('padding-bottom', '16px')
+  expect(json).toHaveStyleRule('margin-top', '32px')
+  expect(json).toHaveStyleRule('margin-bottom', '32px')
+})
+
+test('renders with functional arguments', () => {
+  const json = renderJSON(
+    <div
+      css={t => ({
+        color: 'purple',
+      })}
+    />
+  )
+  expect(json).toHaveStyleRule('color', 'purple')
+})
+
+test('renders without props', () => {
+  const json = renderJSON(<div />)
+  expect(json.type).toBe('div')
+  expect(json.props).toEqual({})
+})
+
+test('renders functional values', () => {
+  const json = renderJSON(
+    <ThemeContext.Provider value={theme}>
+      <div
+        css={{
+          border: t => `1px solid ${t.colors.primary}`,
+        }}
+      />
+    </ThemeContext.Provider>
+  )
+  expect(json).toHaveStyleRule('border', '1px solid #609')
+})
+
+test('picks up fallback theme values for non-standard properties', () => {
+  const json = renderJSON(
+    <ThemeContext.Provider
+      value={{
+        backgroundImage: {
+          cool: 'linear-gradient(cyan, magenta)',
+        },
+      }}
+    >
+      <div
+        css={{
+          backgroundImage: 'cool',
+        }}
+      />
+    </ThemeContext.Provider>
+  )
+  expect(json).toHaveStyleRule(
+    'background-image',
+    'linear-gradient(cyan,magenta)'
+  )
+})
diff --git a/packages/should-forward-prop/test/index.js b/packages/should-forward-prop/test/index.js
new file mode 100644
index 0000000..cf646b8
--- /dev/null
+++ b/packages/should-forward-prop/test/index.js
@@ -0,0 +1,14 @@
+import systemProps from 'styled-system/props'
+import shouldForwardProp from '../index'
+
+test('returns true for valid HTML attributes', () => {
+  const should = shouldForwardProp('href')
+  expect(should).toBe(true)
+})
+
+systemProps.forEach(prop => {
+  test(`returns false for Styled System ${prop} prop`, () => {
+    const should = shouldForwardProp(prop)
+    expect(should).toBe(false)
+  })
+})