IconGrid: massive refactor including component extraction

This patch extracts InfoPanel from IconGridItem, adds style tweaks to
match the spec, and in general reduces prop-drilling by tapping into
recoil state for most config, and making use of IconContext to style
member icons where appropriate :)
This commit is contained in:
rektdeckard
2020-07-29 12:27:50 -04:00
parent c269343014
commit 6941250d10
4 changed files with 283 additions and 94 deletions

View File

@@ -1,21 +1,28 @@
.grid {
/* grid-template-columns: repeat(auto-fill, minmax(160px, 1fr)); */
.grid-container {
padding: 0px 16px 8px;
/* background-color: #35313D;
color: white; */
}
/* display: grid; */
/* grid-template-columns: repeat(auto-fill, 160px);
.grid {
/* display: grid;
grid-area: auto;
grid-template-columns: repeat(auto-fill, minmax(160px, 1fr));
grid-gap: 10px;
grid-auto-rows: minmax(160px, auto); */
grid-auto-rows: 160px;
grid-auto-columns: auto;
max-width: 1120px; */
display: flex;
flex-flow: row wrap;
justify-content: space-between;
margin: 16px;
/* min-height: 100vh; */
max-width: 1120px;
margin: auto;
}
.grid-item {
display: flex;
box-sizing: border-box;
width: 160px;
height: 160px;
flex-direction: column;
@@ -25,20 +32,82 @@
border-radius: 16px;
user-select: none;
cursor: pointer;
/* background-color: rgba(255, 255, 255, 0); */
}
.grid-item:focus {
outline: none;
box-shadow: 0 0 0 2px rgb(246, 242, 243);
}
.grid-item p {
font-size: 12px;
line-height: 16px;
color: #7F7F7F;
margin-top: 12px;
}
.info-box {
display: flex;
margin: 4px;
padding: 16px;
width: 100%;
height: 0px;
margin: 0px;
padding-right: 10%;
border-radius: 16px;
box-shadow: 0 0 0 2px rgb(0, 0, 0);
overflow: hidden;
}
.icon-preview {
height: 396px;
width: 30%;
display: flex;
text-align: center;
flex-direction: column;
align-items: center;
justify-content: center;
}
.icon-preview p {
margin: 12px 0 0;
font-size: 12px;
line-height: 16px;
}
.icon-usage {
flex: 1;
padding: 56px 0px 56px;
/* display: flex; */
/* flex-direction: column; */
/* justify-content: flex-start; */
/* vertical-align: middle; */
}
.snippet {
margin-bottom: 24px;
}
.snippet pre {
text-overflow: ellipsis;
/* overflow-x: auto; */
}
.snippet button {
background-color: transparent;
margin: 0;
padding: 0;
height: 24px;
float: right;
}
.button-row {
display: flex;
}
.button-row button {
background-color: transparent;
font-size: 16px;
line-height: 24px;
margin: 0 48px 0 0;
padding: 0;
height: 48px;
}
.empty-list {

View File

@@ -2,30 +2,30 @@ import React, { useRef, useEffect } from "react";
import { useRecoilValue } from "recoil";
import { motion, useAnimation } from "framer-motion";
import { useWindowSize } from "react-use";
import TinyColor from "tinycolor2";
import { IconContext, WarningTriangle } from "phosphor-react";
import { filteredQueryResultsSelector } from "../../state/selectors";
import {
iconColorAtom,
iconSizeAtom,
styleQueryAtom,
iconSizeAtom,
iconColorAtom,
searchQueryAtom,
} from "../../state/atoms";
import "./IconGrid.css";
import { filteredQueryResultsSelector } from "../../state/selectors";
import GridItem from "./IconGridItem";
import { WarningTriangle, IconProps } from "phosphor-react";
import "./IconGrid.css";
type IconGridProps = {};
const IconGridAnimated: React.FC<IconGridProps> = () => {
const weight = useRecoilValue(styleQueryAtom);
const query = useRecoilValue(searchQueryAtom);
const size = useRecoilValue(iconSizeAtom);
const color = useRecoilValue(iconColorAtom);
const iconProps: IconProps = { weight, color, size };
const query = useRecoilValue(searchQueryAtom);
const isDark = TinyColor(color).isLight();
const { width } = useWindowSize();
const spans = Math.floor((width - 32) / 172);
const spans = Math.floor(Math.min(width - 32, 1120) / 168);
const filteredQueryResults = useRecoilValue(filteredQueryResultsSelector);
@@ -50,23 +50,30 @@ const IconGridAnimated: React.FC<IconGridProps> = () => {
);
return (
<IconContext.Provider value={{ weight, size, color, mirrored: false }}>
<div
className="grid-container"
style={{ backgroundColor: isDark ? "#35313D" : "" }}
>
<motion.div
className="grid"
initial="hidden"
animate={controls}
variants={{}}
>
{filteredQueryResults.map((iconEntry, i) => (
{filteredQueryResults.map((iconEntry, index) => (
<GridItem
key={i}
index={i}
key={index}
index={index}
spans={spans}
isDark={isDark}
{...iconEntry}
{...iconProps}
originOffset={originOffset}
/>
))}
</motion.div>
</div>
</IconContext.Provider>
);
};

View File

@@ -2,6 +2,7 @@ import React, {
useRef,
useLayoutEffect,
useEffect,
useMemo,
MutableRefObject,
} from "react";
import { useRecoilState } from "recoil";
@@ -9,15 +10,22 @@ import { motion, AnimatePresence } from "framer-motion";
import { iconPreviewOpenAtom } from "../../state/atoms";
import { IconProps, Icon } from "phosphor-react";
import InfoPanel from "./InfoPanel";
interface IconGridItemProps extends IconProps {
index: number;
spans: number;
isDark: boolean;
name: string;
Icon: Icon;
originOffset: MutableRefObject<{ top: number; left: number }>;
spans: number;
}
const whileTap = { boxShadow: "0 0 0 4px rgb(73, 70, 80)" };
const transition = { duration: 0.2 };
const originIndex = 0;
const delayPerPixel = 0.0004;
const itemVariants = {
hidden: { opacity: 0 },
visible: (delayRef: any) => ({
@@ -25,30 +33,24 @@ const itemVariants = {
transition: { delay: delayRef.current },
}),
};
const whileHover = { boxShadow: "0 0 0 2px rgb(0, 0, 0)" };
const whileTap = { boxShadow: "0 0 0 4px rgb(139, 0, 139)" };
const transition = { duration: 0.2 };
const originIndex = 0;
const delayPerPixel = 0.0004;
const infoVariants = {
open: { opacity: 1, height: 176, marginTop: 4, marginBottom: 4, padding: 16 },
collapsed: {
opacity: 0,
height: 0,
marginTop: 0,
marginBottom: 0,
padding: 0,
},
};
const IconGridItem: React.FC<IconGridItemProps> = (props) => {
const { index, spans, originOffset, name, Icon, ...iconProps } = props;
const { index, isDark, originOffset, name, Icon } = props;
const [open, setOpen] = useRecoilState(iconPreviewOpenAtom);
const isOpen = open === name;
const delayRef = useRef<number>(0);
const offset = useRef({ top: 0, left: 0 });
const ref = useRef<any>();
const handleOpen = () => setOpen(isOpen ? false : name);
const whileHover = useMemo(
() => ({
backgroundColor: isDark ? "rgb(73, 70, 80)" : "rgb(246, 242, 243)",
}),
[isDark]
);
// The measurement for all elements happens in the layoutEffect cycle
// This ensures that when we calculate distance in the effect cycle
// all elements have already been measured
@@ -64,67 +66,44 @@ const IconGridItem: React.FC<IconGridItemProps> = (props) => {
if (index === originIndex) {
originOffset.current = offset.current;
}
}, []);
}, [index, originOffset]);
useEffect(() => {
const dx = Math.abs(offset.current.left - originOffset.current.left);
const dy = Math.abs(offset.current.top - originOffset.current.top);
const d = Math.sqrt(Math.pow(dx, 2) + Math.pow(dy, 2));
delayRef.current = d * delayPerPixel;
}, []);
}, [originOffset]);
return (
<>
<motion.div
className="grid-item"
ref={ref}
style={{ order: index }}
custom={delayRef}
key={name}
ref={ref}
tabIndex={0}
// onFocus={console.log}
style={{
order: index,
backgroundColor: isDark
? "rgba(73, 70, 80, 0)"
: "rgba(246, 242, 243, 0)",
}}
custom={delayRef}
whileHover={whileHover}
whileTap={whileTap}
transition={transition}
variants={itemVariants}
onClick={() =>
setOpen((openName) => (name === openName ? false : name))
}
onKeyPress={(e) => e.key === "Enter" && handleOpen()}
onClick={handleOpen}
>
<Icon {...iconProps} />
<Icon />
<p>{name}</p>
</motion.div>
<AnimatePresence initial={false}>
{open === name && <InfoPanel {...props} />}
{isOpen && <InfoPanel {...props} />}
</AnimatePresence>
</>
);
};
const InfoPanel: React.FC<IconGridItemProps> = (props) => {
const { index, spans, name, Icon, color, weight } = props;
return (
<motion.section
className="info-box"
animate="open"
exit="collapsed"
variants={infoVariants}
style={{ order: index + (spans - (index % spans)) }}
>
<div style={{ height: "100%" }}>
<Icon color={color} weight={weight} size={128} />
<p>{name}</p>
</div>
<div style={{ flex: 1, padding: 32 }}>
HTML
<pre>{`<i class="ph-${name}${
weight === "regular" ? "" : `-${weight}`
}"></i>`}</pre>
React
<pre>{`<${Icon.displayName} ${
weight === "regular" ? "" : `weight="${weight}"`
}/>`}</pre>
</div>
</motion.section>
);
};
export default IconGridItem;

View File

@@ -0,0 +1,134 @@
import React, { useRef } from "react";
import { useRecoilValue } from "recoil";
import { motion } from "framer-motion";
import { saveAs } from "file-saver";
import { styleQueryAtom, iconSizeAtom, iconColorAtom } from "../../state/atoms";
import { Icon, ArrowUpRightCircle, Copy } from "phosphor-react";
const infoVariants = {
open: {
opacity: 1,
height: 396,
margin: 4,
// transition: { stiffness: 600, damping: 32, duration: 0.2 },
},
collapsed: {
opacity: 0,
height: 0,
margin: 0,
// transition: { stiffness: 600, damping: 32, duration: 0.2 },
},
};
interface InfoPanelProps {
index: number;
spans: number;
isDark: boolean;
name: string;
Icon: Icon;
}
const InfoPanel: React.FC<InfoPanelProps> = (props) => {
const { index, spans, isDark, name, Icon } = props;
const weight = useRecoilValue(styleQueryAtom);
const size = useRecoilValue(iconSizeAtom);
const color = useRecoilValue(iconColorAtom);
const ref = useRef<SVGSVGElement>(null);
const htmlString = `<i class="ph-${name}${
weight === "regular" ? "" : `-${weight}`
}"></i>`;
const reactString = `<${Icon.displayName} size={${size}} color="${color}" ${
weight === "regular" ? "" : `weight="${weight}" `
}/>`;
const handleCopySnippet = (data: string) => {
data && navigator.clipboard.writeText(data);
};
const handleDownloadSVG = () => {
if (!ref.current?.outerHTML) return;
const blob = new Blob([ref.current.outerHTML]);
saveAs(blob, `${name}.svg`);
};
const handleCopySVG = () => {
ref.current && navigator.clipboard.writeText(ref.current.outerHTML);
};
return (
<motion.section
className="info-box"
animate="open"
exit="collapsed"
variants={infoVariants}
style={{
order: index + (spans - (index % spans)),
backgroundColor: isDark ? "rgb(73, 70, 80)" : "rgb(246, 242, 243)",
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">
<div className="snippet">
HTML/CSS
<pre style={{ color: "black" }}>
{htmlString}
<button
title="Copy snippet"
onClick={() => handleCopySnippet(htmlString)}
>
<Copy size={24} color="currentColor" weight="regular" />
</button>
</pre>
</div>
<div className="snippet">
React
<pre style={{ color: "black" }}>
{reactString}
<button
title="Copy snippet"
onClick={() => handleCopySnippet(reactString)}
>
<Copy size={24} color="currentColor" weight="regular" />
</button>
</pre>
</div>
<div className="button-row">
<button
style={{ color: isDark ? "white" : "black" }}
onClick={handleDownloadSVG}
>
<ArrowUpRightCircle
size={32}
style={{ marginRight: 8 }}
color="currentColor"
weight="regular"
/>{" "}
Download SVG
</button>
<button
style={{ color: isDark ? "white" : "black" }}
onClick={handleCopySVG}
>
<Copy
size={32}
style={{ marginRight: 8 }}
color="currentColor"
weight="regular"
/>{" "}
Copy SVG
</button>
</div>
</div>
</motion.section>
);
};
export default InfoPanel;