Skip to content

Commit

Permalink
Add customization controls 🎛 (feathericons#297)
Browse files Browse the repository at this point in the history
* Generate zip dynamically

* Set up sidebar layout

* Scaffold form

* Adjust layout

* Set up formik

* Virtualize grid

* Change form labels

* Default OptionsContext inital value to {}

* Disabled useQueryParam

* Don't display json of form values

* Make carbon ad width 100%

* Move customize controls into seperate component

* Download zip with customized icons

* Update download and copy calls to use custom options

* Add back query params

* Adjust search input bg gradient

* Fix build

* Update row height

* Remove cap and join options
  • Loading branch information
colebemis authored Nov 30, 2019
1 parent fcee295 commit 335d6f5
Show file tree
Hide file tree
Showing 13 changed files with 578 additions and 163 deletions.
3 changes: 2 additions & 1 deletion gatsby-browser.js
Original file line number Diff line number Diff line change
@@ -1,9 +1,10 @@
import React from 'react'
import { globalHistory } from '@reach/router'
import { QueryParamProvider } from 'use-query-params'
import { OptionsProvider } from './src/components/OptionsContext'

export const wrapRootElement = ({ element }) => (
<QueryParamProvider reachHistory={globalHistory}>
{element}
<OptionsProvider>{element}</OptionsProvider>
</QueryParamProvider>
)
10 changes: 7 additions & 3 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -13,24 +13,28 @@
"@emotion/core": "^10.0.22",
"@mdx-js/react": "^1.5.1",
"@primer/primitives": "^2.0.0",
"@rebass/forms": "^4.0.6",
"@styled-system/css": "^5.0.23",
"classnames": "2.2.6",
"copy-to-clipboard": "^3.0.8",
"downloadjs": "1.4.7",
"feather-icons": "^4.24.1",
"formik": "^2.0.3",
"fuse.js": "3.4.5",
"gatsby": "^2.17.11",
"gatsby-plugin-google-analytics": "^2.1.25",
"gatsby-plugin-manifest": "^2.2.27",
"gatsby-plugin-react-helmet": "^3.1.14",
"gatsby-plugin-theme-ui": "^0.2.43",
"gatsby-source-filesystem": "^2.1.36",
"gatsby-source-filesystem": "^2.1.0",
"jszip": "^3.2.2",
"lodash.isempty": "^4.4.0",
"lodash.isequal": "^4.5.0",
"polished": "^3.4.2",
"react": "^16.8.6",
"react-dom": "^16.8.6",
"react-helmet": "5.2.1",
"react-virtualized": "^9.21.2",
"react-helmet": "5.2.0",
"react-virtualized": "^9.21.1",
"theme-ui": "^0.2.46",
"use-query-params": "^0.4.4"
},
Expand Down
2 changes: 1 addition & 1 deletion src/components/Button.js
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ function Button({ as: Component, ...props }) {
cursor: 'pointer',
'&:disabled': {
opacity: 0.5,
cursor: 'default',
pointerEvents: 'none',
},
}}
{...props}
Expand Down
3 changes: 1 addition & 2 deletions src/components/CarbonAd.js
Original file line number Diff line number Diff line change
Expand Up @@ -17,8 +17,7 @@ function CarbonAd(props) {
<div
ref={containerRef}
sx={{
width: 400,
maxWidth: '100%',
width: '100%',
minHeight: 132,
padding: 4,
fontSize: 1,
Expand Down
114 changes: 114 additions & 0 deletions src/components/Customize.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,114 @@
/** @jsx jsx */
import { Input, Label, Select, Slider } from '@rebass/forms'
import { useFormik } from 'formik'
import isEqual from 'lodash.isequal'
import React from 'react'
import { jsx } from 'theme-ui'
import Button from './Button'
import { useOptions } from './OptionsContext'

const INITIAL_VALUES = {
size: 24,
strokeWidth: 2,
strokeColor: 'currentColor',
}

function Customize() {
const { values, handleChange, handleReset } = useFormik({
initialValues: INITIAL_VALUES,
})

const { setOptions } = useOptions()

React.useEffect(() => setOptions(values), [setOptions, values])

return (
<form sx={{ display: 'grid', gridGap: 4 }}>
<div
sx={{
display: 'flex',
alignItems: 'center',
justifyContent: 'space-between',
}}
>
<h2 sx={{ margin: 0, fontSize: 4, fontWeight: 'bold' }}>Customize</h2>
<Button
onClick={handleReset}
disabled={isEqual(values, INITIAL_VALUES)}
sx={{
variant: 'buttons.outline',
paddingY: 1,
paddingX: 2,
fontSize: 1,
}}
>
Reset
</Button>
</div>
<div>
<Label htmlFor="size">Size</Label>
<div
sx={{
display: 'grid',
gridGap: 4,
gridTemplateColumns: '1fr 48px',
justifyItems: 'end',
alignItems: 'center',
}}
>
<Slider
id="size"
min="12"
max="100"
step="4"
value={values.size}
onChange={handleChange}
></Slider>
{values.size}px
</div>
</div>
<div>
<Label htmlFor="strokeWidth">Stroke width</Label>
<div
sx={{
display: 'grid',
gridGap: 4,
gridTemplateColumns: '1fr 48px',
justifyItems: 'end',
alignItems: 'center',
}}
>
<Slider
id="strokeWidth"
min="0.5"
max="3"
step="0.5"
value={values.strokeWidth}
onChange={handleChange}
></Slider>
{values.strokeWidth}px
</div>
</div>
<div>
<Label htmlFor="strokeColor">Color</Label>
<div
sx={{
display: 'grid',
gridGap: 4,
gridTemplateColumns: '1fr 48px',
}}
>
<Input
id="strokeColor"
placeholder="#000000"
value={values.strokeColor}
onChange={handleChange}
></Input>
<div sx={{ backgroundColor: values.strokeColor, borderRadius: 1 }} />
</div>
</div>
</form>
)
}

export default Customize
18 changes: 14 additions & 4 deletions src/components/Hero.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,14 +2,26 @@
import download from 'downloadjs'
import { icons } from 'feather-icons'
import JSZip from 'jszip'
import isEmpty from 'lodash.isempty'
import { jsx } from 'theme-ui'
import logDownload from '../utils/logDownload'
import logOutboundLink from '../utils/logOutboundLink'
import Button from './Button'
import CarbonAd from './CarbonAd'
import { useOptions } from './OptionsContext'
import OutboundLink from './OutboundLink'

function Hero() {
const { options } = useOptions()

const attrs = !isEmpty(options)
? {
width: options.size,
height: options.size,
stroke: options.strokeColor,
'stroke-width': options.strokeWidth,
}
: {}

return (
<div
sx={{
Expand Down Expand Up @@ -54,7 +66,7 @@ function Hero() {
</Button>
<Button
onClick={async () => {
const zip = await generateZip()
const zip = await generateZip(attrs)
download(zip, 'feather.zip')
logDownload('all')
}}
Expand All @@ -63,8 +75,6 @@ function Hero() {
Download all
</Button>
</div>

<CarbonAd sx={{ marginTop: 8 }} />
</div>
)
}
Expand Down
24 changes: 19 additions & 5 deletions src/components/IconGrid.js
Original file line number Diff line number Diff line change
@@ -1,24 +1,38 @@
/** @jsx jsx */
import copy from 'copy-to-clipboard'
import download from 'downloadjs'
import isEmpty from 'lodash.isempty'
import { arrayOf, func, shape, string } from 'prop-types'
import React from 'react'
import { AutoSizer, List, WindowScroller } from 'react-virtualized'
import { jsx } from 'theme-ui'
import logCopy from '../utils/logCopy'
import logDownload from '../utils/logDownload'
import IconTile from './IconTile'
import React from 'react'
import { AutoSizer, List, WindowScroller } from 'react-virtualized'
import { useOptions } from './OptionsContext'

// IconGrid might need to display a lot of icons (>200).
// To avoid an excessive DOM size, we use react-virtualized
// to only render the icons that are visible on the screen.

const ROW_HEIGHT = 160
const ROW_HEIGHT = 180
const MAX_COLUMN_WIDTH = 160

function IconGrid({ icons }) {
// Initialize numColumns to an arbitrary number.
const [numColumns, setNumColumns] = React.useState(1)

const { options } = useOptions()

const attrs = !isEmpty(options)
? {
width: options.size,
height: options.size,
stroke: options.strokeColor,
'stroke-width': options.strokeWidth,
}
: {}

return (
<div sx={{ margin: -2, minHeight: ROW_HEIGHT }}>
<WindowScroller>
Expand Down Expand Up @@ -71,11 +85,11 @@ function IconGrid({ icons }) {
title={`Download ${icon.name}.svg`}
onClick={event => {
if (event.shiftKey) {
copy(icon.toSvg())
copy(icon.toSvg(attrs))
logCopy(icon.name)
} else {
download(
icon.toSvg(),
icon.toSvg(attrs),
`${icon.name}.svg`,
'image/svg+xml',
)
Expand Down
11 changes: 10 additions & 1 deletion src/components/IconTile.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,10 @@
import { func, string } from 'prop-types'
import { jsx } from 'theme-ui'
import Icon from './Icon'
import { useOptions } from './OptionsContext'

function IconTile({ name, onClick, ...props }) {
const { options } = useOptions()
return (
<div
role="button"
Expand Down Expand Up @@ -44,10 +46,17 @@ function IconTile({ name, onClick, ...props }) {
justifyContent: 'center',
}}
>
<Icon name={name} />
<Icon
name={name}
width={options.size}
height={options.size}
strokeWidth={options.strokeWidth}
stroke={options.strokeColor}
/>
</div>
<span
sx={{
flex: '0 0 auto',
fontSize: 1,
paddingX: 4,
textAlign: 'center',
Expand Down
16 changes: 16 additions & 0 deletions src/components/OptionsContext.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
import React from 'react'

const OptionsContext = React.createContext({})

export function OptionsProvider({ children }) {
const [options, setOptions] = React.useState({})
return (
<OptionsContext.Provider value={{ options, setOptions }}>
{children}
</OptionsContext.Provider>
)
}

export function useOptions() {
return React.useContext(OptionsContext)
}
15 changes: 15 additions & 0 deletions src/components/Sidebar.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
/** @jsx jsx */
import { jsx } from 'theme-ui'
import Customize from './Customize'
import CarbonAd from './CarbonAd'

function Sidebar() {
return (
<div sx={{ paddingY: 5, paddingX: 4 }}>
<Customize />
<CarbonAd sx={{ marginTop: 6 }} />
</div>
)
}

export default Sidebar
26 changes: 26 additions & 0 deletions src/gatsby-plugin-theme-ui/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -54,4 +54,30 @@ export default {
},
},
},
forms: {
label: {
fontSize: 1,
mb: 1,
},
field: {
paddingX: 3,
backgroundColor: 'background',
borderColor: 'border',
borderRadius: 1,
':focus': {
borderColor: 'primary',
outline: 'none',
boxShadow: theme => `0 0 0 2px ${theme.colors.primary}`,
},
},
input: {
variant: 'forms.field',
},
select: {
variant: 'forms.field',
},
slider: {
backgroundColor: 'border',
},
},
}
Loading

0 comments on commit 335d6f5

Please sign in to comment.