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:
@@ -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": {
|
||||||
|
|||||||
@@ -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 {
|
||||||
|
|||||||
@@ -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 {
|
||||||
|
|||||||
@@ -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>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
Reference in New Issue
Block a user