Add customization options to the icon display and download (#113)

* feat: ability to customize icons before downloading PNG or SVG

* feat: change color input to be a graphical color picker

* feat: tweak appearance of new inputs

* fix: switch to correct Chakra-UI@Next slider syntax

* fix: make default color `currentColor` and also add Reset button
This commit is contained in:
Frank Riccobono
2020-10-31 07:53:25 -04:00
committed by GitHub
parent f3c3fea228
commit 51fd3af446
11 changed files with 437 additions and 38 deletions

View File

@@ -0,0 +1,63 @@
import { SyntheticEvent, useEffect, useRef, useState } from 'react';
import { FormLabel, Icon, Input, InputGroup, InputLeftElement } from '@chakra-ui/core';
import { CustomPicker } from 'react-color';
const { Saturation, Hue } = require('react-color/lib/components/common');
type ColorPickerProps = {
value: string;
hex: string;
hsl: string;
hsv: string;
onChange: (s: string, e: SyntheticEvent) => void;
};
function ColorPicker({ hsv, hsl, onChange, hex, value: color }: ColorPickerProps) {
const [value, setValue] = useState(color);
const input = useRef<HTMLInputElement>(null);
useEffect(() => {
if (color !== value && input.current !== document.activeElement) {
setValue(color === "currentColor" ? color : String(color).toUpperCase());
}
}, [color]);
const handleChange = (e) => {
let value = e.target.value;
setValue(value);
onChange(value, e);
};
return (
<div>
<FormLabel htmlFor="color" fontWeight={'bold'}>
Color
</FormLabel>
<InputGroup>
<InputLeftElement
children={
<Icon>
<rect x={0} width={24} y={0} height={24} fill={value} rx={2} />
</Icon>
}
/>
<Input value={value} name="color" onChange={handleChange} ref={input} />
</InputGroup>
<div
style={{
width: '100%',
paddingBottom: '75%',
position: 'relative',
overflow: 'hidden',
}}
>
<Saturation hsl={hsl} hsv={hsv} onChange={onChange} />
</div>
<div style={{ minHeight: '1em', position: 'relative', margin: 2 }}>
<Hue hsl={hsl} onChange={onChange} direction={'horizontal'} />
</div>
</div>
);
}
export default CustomPicker(ColorPicker);

View File

@@ -0,0 +1,47 @@
import { createContext, useState } from 'react';
interface ICustomIconStyle {
color: string;
setColor: (s: string) => void;
strokeWidth: number;
setStroke: (n: number) => void;
size: number;
setSize: (n: number) => void;
resetStyle: () => void;
}
const DEFAULT_STYLE = {
color: 'currentColor',
strokeWidth: 2,
size: 24,
}
export const IconStyleContext = createContext<ICustomIconStyle>({
color: 'currentColor',
setColor: (s: string) => null,
strokeWidth: 2,
setStroke: (n: number) => null,
size: 24,
setSize: (n: number) => null,
resetStyle: ()=>null
});
export function CustomizeIconContext({ children }) {
const [color, setColor] = useState(DEFAULT_STYLE.color);
const [stroke, setStroke] = useState(DEFAULT_STYLE.strokeWidth);
const [size, setSize] = useState(DEFAULT_STYLE.size);
function resetStyle(){
setColor(DEFAULT_STYLE.color);
setStroke(DEFAULT_STYLE.strokeWidth);
setSize(DEFAULT_STYLE.size);
}
return (
<IconStyleContext.Provider
value={{ color, setColor, strokeWidth: stroke, setStroke, size, setSize, resetStyle }}
>
{children}
</IconStyleContext.Provider>
);
}

View File

@@ -1,14 +1,8 @@
import {
Button,
Flex,
Stack,
Text,
Link,
} from "@chakra-ui/core";
import {Button, Flex, Link, Stack, Text,} from "@chakra-ui/core";
import download from "downloadjs";
import JSZip from "jszip";
import { Download, GitHub } from 'lucide-react';
import theme from "../lib/theme";
import {IconCustomizerDrawer} from "./IconCustomizerDrawer";
function generateZip(icons) {
const zip = new JSZip();
@@ -44,6 +38,7 @@ const Header = ({ data }) => {
>
Download all
</Button>
<IconCustomizerDrawer/>
<Button
as="a"
leftIcon={<GitHub/>}

View File

@@ -0,0 +1,96 @@
import { useContext, useState } from 'react';
import { IconStyleContext } from './CustomizeIconContext';
import { Edit } from 'lucide-react';
import {
Button,
Drawer,
DrawerBody,
DrawerCloseButton,
DrawerContent,
DrawerHeader,
DrawerOverlay,
FormControl,
FormLabel,
Grid,
Slider,
SliderFilledTrack,
SliderThumb,
SliderTrack,
Flex,
Text,
} from '@chakra-ui/core';
import ColorPicker from './ColorPicker';
export function IconCustomizerDrawer() {
const [showCustomize, setShowCustomize] = useState(false);
const { color, setColor, size, setSize, strokeWidth, setStroke, resetStyle } = useContext(IconStyleContext);
return (
<>
<Button as="a" leftIcon={<Edit />} size="lg" onClick={() => setShowCustomize(true)}>
Customize
</Button>
<Drawer isOpen={showCustomize} placement="right" onClose={() => setShowCustomize(false)}>
<DrawerOverlay />
<DrawerContent>
<DrawerCloseButton />
<DrawerHeader>Customize Icons</DrawerHeader>
<DrawerBody>
<Grid gridGap={'1em'}>
<FormControl>
<ColorPicker
color={color}
value={color}
onChangeComplete={(col) => setColor(col.hex)}
/>
</FormControl>
<FormControl>
<FormLabel htmlFor="stroke">
<Flex>
<Text flexGrow={1} fontWeight={'bold'}>
Stroke
</Text>
<Text>{strokeWidth}px</Text>
</Flex>
</FormLabel>
<Slider
value={strokeWidth}
onChange={setStroke}
min={0.5}
max={3}
step={0.5}
name={'stroke'}
>
<SliderTrack>
<SliderFilledTrack bg={color} />
</SliderTrack>
<SliderThumb />
</Slider>
</FormControl>
<FormControl>
<FormLabel htmlFor="size">
<Flex>
<Text flexGrow={1} fontWeight={'bold'}>
Size
</Text>
<Text>{size}px</Text>
</Flex>
</FormLabel>
<Slider value={size} onChange={setSize} min={12} max={64} step={1} name={'size'}>
<SliderTrack>
<SliderFilledTrack bg={color} />
</SliderTrack>
<SliderThumb />
</Slider>
</FormControl>
<FormControl>
<Button onClick={resetStyle}>Reset</Button>
</FormControl>
</Grid>
</DrawerBody>
</DrawerContent>
</Drawer>
</>
);
}

View File

@@ -4,11 +4,21 @@ import theme from "../lib/theme";
import download from 'downloadjs';
import copy from "copy-to-clipboard";
import { X as Close } from 'lucide-react';
import {useContext, useRef} from "react";
import {IconStyleContext} from "./CustomizeIconContext";
import {IconWrapper} from "./IconWrapper";
type IconDownload = {
src: string;
name: string;
};
const IconDetailOverlay = ({ isOpen = true, onClose, icon }) => {
const toast = useToast();
const { colorMode } = useColorMode();
const { tags = [], name } = icon;
const {color, strokeWidth, size} = useContext(IconStyleContext);
const iconRef = useRef<SVGSVGElement>(null);
const { transform, opacity } = useSpring({
opacity: isOpen ? 1 : 0,
@@ -34,12 +44,12 @@ const IconDetailOverlay = ({ isOpen = true, onClose, icon }) => {
minWidth: "160px",
maxHeight: "240px",
maxWidth: "240px",
color: (isLight ? theme.colors.gray[800] : theme.colors.white),
color: color,
});
const downloadIcon = ({src, name}) => download(src, `${name}.svg`, 'image/svg+xml');
const downloadIcon = ({src, name} : IconDownload) => download(src, `${name}.svg`, 'image/svg+xml');
const copyIcon = ({src, name}) => {
const copyIcon = ({src, name} : IconDownload) => {
copy(src);
toast({
title: "Copied!",
@@ -49,10 +59,10 @@ const IconDetailOverlay = ({ isOpen = true, onClose, icon }) => {
});
}
const donwloadPNG = ({src, name}) => {
const downloadPNG = ({src, name}: IconDownload) => {
const canvas = document.createElement('canvas');
canvas.width = 24;
canvas.height = 24;
canvas.width = size;
canvas.height = size;
const ctx = canvas.getContext("2d");
const image = new Image();
@@ -129,10 +139,18 @@ const IconDetailOverlay = ({ isOpen = true, onClose, icon }) => {
padding={0}
>
<div
dangerouslySetInnerHTML={{ __html: icon.src }}
style={iconStyling(colorMode == "light")}
className="icon-large"
/>
>
<IconWrapper
content={icon.content}
stroke={color}
strokeWidth={strokeWidth}
height={size}
width={size}
ref={iconRef}
/>
</div>
<svg className="icon-grid" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke={colorMode == "light" ? '#E2E8F0' : theme.colors.gray[600]} strokeWidth="0.1" xmlns="http://www.w3.org/2000/svg">
{ Array.from({ length:23 }, (_, i) => (
@@ -169,13 +187,13 @@ const IconDetailOverlay = ({ isOpen = true, onClose, icon }) => {
</Button> */}
</Box>
<ButtonGroup spacing={4}>
<Button variant="solid" onClick={() => downloadIcon(icon)} mb={1}>
<Button variant="solid" onClick={() => downloadIcon({src: iconRef.current.outerHTML, name: icon.name})} mb={1}>
Download SVG
</Button>
<Button variant="solid" onClick={() => copyIcon(icon)} mb={1}>
<Button variant="solid" onClick={() => copyIcon({src: iconRef.current.outerHTML, name: icon.name})} mb={1}>
Copy SVG
</Button>
<Button variant="solid" onClick={() => donwloadPNG(icon)} mb={1}>
<Button variant="solid" onClick={() => downloadPNG({src: iconRef.current.outerHTML, name: icon.name})} mb={1}>
Download PNG
</Button>
</ButtonGroup>

View File

@@ -2,9 +2,13 @@ import { Button, Flex, Grid, Text, useToast } from "@chakra-ui/core";
import download from 'downloadjs';
import Link from 'next/link'
import copy from "copy-to-clipboard";
import {useContext} from "react";
import {IconStyleContext} from "./CustomizeIconContext";
import {IconWrapper} from "./IconWrapper";
const IconList = ({icons}) => {
const toast = useToast();
const {color, size, strokeWidth} = useContext(IconStyleContext);
return (
<Grid
@@ -15,7 +19,7 @@ const IconList = ({icons}) => {
{ icons.map((icon) => {
// @ts-ignore
const actualIcon = icon.item ? icon.item : icon;
const { name, src } = actualIcon;
const { name, content } = actualIcon;
return (
<Link key={name} href={`/?iconName=${name}`} as={`/icon/${name}`} scroll={false}>
@@ -46,7 +50,13 @@ const IconList = ({icons}) => {
alignItems="center"
>
<Flex direction="column" align="center" justify="center">
<div dangerouslySetInnerHTML={{ __html: src }} />
<IconWrapper
content={content}
stroke={color}
strokeWidth={strokeWidth}
height={size}
width={size}
/>
<Text marginTop={5}>{name}</Text>
</Flex>
</Button>

View File

@@ -0,0 +1,27 @@
import { forwardRef, SVGProps } from 'react';
interface IconWrapperProps extends SVGProps<SVGSVGElement> {
content: string;
}
export const IconWrapper = forwardRef<SVGSVGElement, IconWrapperProps>((props, ref) => {
const defaultAttrs : SVGProps<SVGSVGElement>= {
xmlns: 'http://www.w3.org/2000/svg',
width: '24px',
height: '24px',
viewBox: '0 0 24 24',
fill: 'none',
stroke: 'currentColor',
strokeWidth: '2px',
strokeLinecap: 'round',
strokeLinejoin: 'round',
};
const { content, ...rest } = props;
const attrs = {
...defaultAttrs,
...rest,
};
return <svg ref={ref} {...attrs} dangerouslySetInnerHTML={{ __html: content }} />;
});

View File

@@ -1,5 +1,6 @@
import fs from "fs";
import path from "path";
import cheerio from 'cheerio';
import tags from '../../../tags.json';
const directory = path.join(process.cwd(), "../icons");
@@ -16,10 +17,14 @@ export function getData(name) {
const fullPath = path.join(directory, `${name}.svg`);
const fileContents = fs.readFileSync(fullPath, "utf8");
const $ = cheerio.load(fileContents);
const content = $("svg").html();
return {
name,
tags: tags[name] || [],
src: fileContents,
content: content
};
}

View File

@@ -5,6 +5,7 @@ import IconOverview from "../components/IconOverview";
import IconDetailOverlay from "../components/IconDetailOverlay";
import { useRouter } from "next/router";
import Header from "../components/Header";
import {CustomizeIconContext} from "../components/CustomizeIconContext";
const IndexPage = ({ data }) => {
const router = useRouter();
@@ -12,13 +13,15 @@ const IndexPage = ({ data }) => {
return (
<Layout>
<IconDetailOverlay
isOpen={!!router.query.iconName}
icon={getIcon(router.query.iconName)}
onClose={() => router.push('/')}
/>
<Header {...{data}}/>
<IconOverview {...{data}}/>
<CustomizeIconContext>
<IconDetailOverlay
isOpen={!!router.query.iconName}
icon={getIcon(router.query.iconName)}
onClose={() => router.push('/')}
/>
<Header {...{data}}/>
<IconOverview {...{data}}/>
</CustomizeIconContext>
</Layout>
);
};