InfoPanel: massive overhaul to support mobile size and PNG download

This patch reworks the mobile breakpoint to allow whitespace wrapping of
code snippets and does away with horizontal scroll. Overall, the
usability and intuitiveness is much better, though readability of the
code itself takes a hit.

In addition, we added the ability to download an icon as a PNG thanks to
the svg2png-converter library. PNGs adopt the current preview weight and
color, and are sized at 256x256.
This commit is contained in:
rektdeckard
2020-10-04 23:15:51 -04:00
parent bee9f1cbc0
commit 50b603b525
4 changed files with 106 additions and 55 deletions

View File

@@ -31,6 +31,7 @@
"react-scripts": "3.4.1", "react-scripts": "3.4.1",
"react-use": "^15.3.2", "react-use": "^15.3.2",
"recoil": "^0.0.10", "recoil": "^0.0.10",
"svg2png-converter": "^1.0.0",
"tinycolor2": "^1.4.1" "tinycolor2": "^1.4.1"
}, },
"scripts": { "scripts": {

View File

@@ -28,6 +28,7 @@ pre {
background-color: white; background-color: white;
border-radius: 6px; border-radius: 6px;
border: 1px solid #e1d4d7; border: 1px solid #e1d4d7;
white-space: pre-wrap;
} }
input { input {

View File

@@ -48,11 +48,9 @@
display: flex; display: flex;
width: 100%; width: 100%;
height: 0px; height: 0px;
margin: 0px; margin: 0 4px;
border-radius: 16px; border-radius: 16px;
background-color: rgba(163, 159, 171, 0.1); background-color: rgba(163, 159, 171, 0.1);
overflow-y: hidden;
overflow-x: auto;
} }
@media screen and (max-width: 1023px) { @media screen and (max-width: 1023px) {
@@ -90,8 +88,8 @@
} }
.snippet pre { .snippet pre {
/* white-space: nowrap; */ display: flex;
/* overflow: hidden; */ align-items: center;
text-overflow: ellipsis; text-overflow: ellipsis;
color: black; color: black;
user-select: all; user-select: all;
@@ -111,12 +109,15 @@
} }
} }
.snippet span {
flex: 1;
}
.snippet button { .snippet button {
background-color: transparent; background-color: transparent;
margin: 0; margin: 0;
padding: 0; padding: 0;
height: 24px; height: 24px;
float: right;
cursor: pointer; cursor: pointer;
} }
@@ -126,6 +127,7 @@
.button-row { .button-row {
display: flex; display: flex;
flex-wrap: wrap;
} }
.button-row button { .button-row button {

View File

@@ -1,6 +1,7 @@
import React, { useRef } from "react"; import React, { useRef } from "react";
import { useRecoilValue, useSetRecoilState } from "recoil"; import { useRecoilValue, useSetRecoilState } from "recoil";
import { motion } from "framer-motion"; import { motion } from "framer-motion";
import { Svg2Png } from "svg2png-converter";
import { saveAs } from "file-saver"; import { saveAs } from "file-saver";
import { Icon, Copy, X, CheckCircle, Download } from "phosphor-react"; import { Icon, Copy, X, CheckCircle, Download } from "phosphor-react";
@@ -12,21 +13,32 @@ import {
} from "../../state/atoms"; } from "../../state/atoms";
import useTransientState from "../../hooks/useTransientState"; import useTransientState from "../../hooks/useTransientState";
const infoVariants = { const panelVariants = {
open: { open: {
opacity: 1, opacity: 1,
height: 496, height: "100%",
margin: 4, marginTop: 4,
marginBottom: 4,
// transition: { stiffness: 600, damping: 32, duration: 0.2 }, // transition: { stiffness: 600, damping: 32, duration: 0.2 },
}, },
collapsed: { collapsed: {
opacity: 0, opacity: 0,
height: 0, height: 0,
margin: 0, marginTop: 0,
marginBottom: 0,
// transition: { stiffness: 600, damping: 32, duration: 0.2 }, // transition: { stiffness: 600, damping: 32, duration: 0.2 },
}, },
}; };
const contentVariants = {
open: { opacity: 1, transition: { duration: 0.2 } },
collapsed: { opacity: 0, transition: { duration: 0.1 } },
};
const buttonColor = "#35313D";
const successColor = "#1FA647";
const disabledColor = "#B7B7B7";
interface InfoPanelProps { interface InfoPanelProps {
index: number; index: number;
spans: number; spans: number;
@@ -44,6 +56,12 @@ const InfoPanel: React.FC<InfoPanelProps> = (props) => {
const [copied, setCopied] = useTransientState<string | false>(false, 2000); const [copied, setCopied] = useTransientState<string | false>(false, 2000);
const ref = useRef<SVGSVGElement>(null); const ref = useRef<SVGSVGElement>(null);
const buttonBarStyle = { color: isDark ? "white" : buttonColor };
const snippetButtonStyle =
weight === "duotone"
? { color: disabledColor, "user-select": "none" }
: { color: buttonColor };
const snippets = { const snippets = {
html: html:
weight === "duotone" weight === "duotone"
@@ -69,6 +87,14 @@ const InfoPanel: React.FC<InfoPanelProps> = (props) => {
data && void navigator.clipboard?.writeText(data); data && void navigator.clipboard?.writeText(data);
}; };
const handleCopySVG = (
event: React.MouseEvent<HTMLButtonElement, MouseEvent>
) => {
event.currentTarget.blur();
setCopied("svg");
ref.current && void navigator.clipboard?.writeText(ref.current.outerHTML);
};
const handleDownloadSVG = ( const handleDownloadSVG = (
event: React.MouseEvent<HTMLButtonElement, MouseEvent> event: React.MouseEvent<HTMLButtonElement, MouseEvent>
) => { ) => {
@@ -78,12 +104,16 @@ const InfoPanel: React.FC<InfoPanelProps> = (props) => {
saveAs(blob, `${name}${weight === "regular" ? "" : `-${weight}`}.svg`); saveAs(blob, `${name}${weight === "regular" ? "" : `-${weight}`}.svg`);
}; };
const handleCopySVG = ( const handleDownloadPNG = async (
event: React.MouseEvent<HTMLButtonElement, MouseEvent> event: React.MouseEvent<HTMLButtonElement, MouseEvent>
) => { ) => {
event.currentTarget.blur(); event.currentTarget.blur();
setCopied("svg"); if (!ref.current?.outerHTML) return;
ref.current && void navigator.clipboard?.writeText(ref.current.outerHTML); Svg2Png.save(
ref.current,
`${name}${weight === "regular" ? "" : `-${weight}`}.png`,
{ scaleX: 1.334, scaleY: 1.334 }
);
}; };
return ( return (
@@ -91,31 +121,41 @@ const InfoPanel: React.FC<InfoPanelProps> = (props) => {
className="info-box" className="info-box"
animate="open" animate="open"
exit="collapsed" exit="collapsed"
variants={infoVariants} variants={panelVariants}
style={{ style={{
order: index + (spans - (index % spans)), order: index + (spans - (index % spans)),
color: isDark ? "white" : "black", color: isDark ? "white" : "black",
}} }}
> >
<div className="icon-preview"> <motion.div
<div> initial="collapsed"
animate="open"
exit="collapsed"
variants={contentVariants}
className="icon-preview"
>
<Icon ref={ref} color={color} weight={weight} size={192} /> <Icon ref={ref} color={color} weight={weight} size={192} />
<p>{name}</p> <p>{name}</p>
</div> </motion.div>
</div> <motion.div
<div className="icon-usage"> initial="collapsed"
animate="open"
exit="collapsed"
variants={contentVariants}
className="icon-usage"
>
<div className="snippet"> <div className="snippet">
React React
<pre tabIndex={0}> <pre tabIndex={0}>
{snippets.react} <span>{snippets.react}</span>
<button <button
title="Copy snippet" title="Copy snippet"
onClick={(e) => handleCopySnippet(e, "react")} onClick={(e) => handleCopySnippet(e, "react")}
> >
{copied === "react" ? ( {copied === "react" ? (
<CheckCircle size={24} color="#1FA647" weight="fill" /> <CheckCircle size={24} color={successColor} weight="fill" />
) : ( ) : (
<Copy size={24} color="currentColor" weight="fill" /> <Copy size={24} color={buttonColor} weight="fill" />
)} )}
</button> </button>
</pre> </pre>
@@ -123,64 +163,70 @@ const InfoPanel: React.FC<InfoPanelProps> = (props) => {
<div className="snippet"> <div className="snippet">
Vue Vue
<pre tabIndex={0}> <pre tabIndex={0}>
{snippets.vue} <span>{snippets.vue}</span>
<button <button
title="Copy snippet" title="Copy snippet"
onClick={(e) => handleCopySnippet(e, "vue")} onClick={(e) => handleCopySnippet(e, "vue")}
> >
{copied === "vue" ? ( {copied === "vue" ? (
<CheckCircle size={24} color="#1FA647" weight="fill" /> <CheckCircle size={24} color={successColor} weight="fill" />
) : ( ) : (
<Copy size={24} color="currentColor" weight="fill" /> <Copy size={24} color={buttonColor} weight="fill" />
)} )}
</button> </button>
</pre> </pre>
</div> </div>
<div className="snippet"> <div className="snippet">
HTML/CSS HTML/CSS
<pre <pre tabIndex={0} style={snippetButtonStyle}>
tabIndex={0} <span>{snippets.html}</span>
style={weight === "duotone" ? { color: "#B7B7B7" } : {}}
>
{snippets.html}
<button <button
title="Copy snippet" title="Copy snippet"
onClick={(e) => handleCopySnippet(e, "html")} onClick={(e) => handleCopySnippet(e, "html")}
disabled={weight === "duotone"} disabled={weight === "duotone"}
style={snippetButtonStyle}
> >
{copied === "html" ? ( {copied === "html" ? (
<CheckCircle size={24} color="#1FA647" weight="fill" /> <CheckCircle size={24} color={successColor} weight="fill" />
) : ( ) : (
<Copy size={24} color="currentColor" weight="fill" /> <Copy
size={24}
color={snippetButtonStyle.color}
weight="fill"
/>
)} )}
</button> </button>
</pre> </pre>
</div> </div>
<div className="button-row"> <div className="button-row">
<button <button style={buttonBarStyle} onClick={handleDownloadPNG}>
style={{ color: isDark ? "white" : "black" }} <Download size={32} color="currentColor" weight="fill" /> Download
onClick={handleDownloadSVG} PNG
> </button>
<button style={buttonBarStyle} onClick={handleDownloadSVG}>
<Download size={32} color="currentColor" weight="fill" /> Download <Download size={32} color="currentColor" weight="fill" /> Download
SVG SVG
</button> </button>
<button <button style={buttonBarStyle} onClick={handleCopySVG}>
style={{ color: isDark ? "white" : "black" }}
onClick={handleCopySVG}
>
{copied === "svg" ? ( {copied === "svg" ? (
<CheckCircle size={32} color="#1FA647" weight="fill" /> <CheckCircle size={32} color={successColor} weight="fill" />
) : ( ) : (
<Copy size={32} color="currentColor" weight="fill" /> <Copy size={32} color="currentColor" weight="fill" />
)} )}
{copied === "svg" ? "Copied!" : "Copy SVG"} {copied === "svg" ? "Copied!" : "Copy SVG"}
</button> </button>
</div> </div>
</div> </motion.div>
<motion.span
initial="collapsed"
animate="open"
exit="collapsed"
variants={contentVariants}
>
<X <X
className="close-icon" className="close-icon"
tabIndex={0} tabIndex={0}
color="currentColor" color={buttonBarStyle.color}
size={32} size={32}
weight="fill" weight="fill"
onClick={() => setOpen(false)} onClick={() => setOpen(false)}
@@ -188,6 +234,7 @@ const InfoPanel: React.FC<InfoPanelProps> = (props) => {
e.key === "Enter" && setOpen(false); e.key === "Enter" && setOpen(false);
}} }}
/> />
</motion.span>
</motion.section> </motion.section>
); );
}; };