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

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

View File

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