Massive interactivity updates to all components

This commit is contained in:
rektdeckard
2020-07-24 14:40:07 -04:00
parent 8ae4cb2b81
commit ecb51191d8
14 changed files with 340 additions and 82 deletions

View File

@@ -17,11 +17,9 @@ const ColorInput: React.FC<ColorInputProps> = () => {
return (
<div>
<label htmlFor="color-picker" hidden>
Icon Color
</label>
<input
id="color-picker"
aria-label="Icon Color"
type="color"
onChange={handleColorChange}
value={color}

View File

@@ -10,9 +10,12 @@ const Header: React.FC<HeaderProps> = () => {
<h1>Phosphor Icons</h1>
</div>
<div style={{ paddingRight: 32, textAlign: "end" }}>
<button>Download All</button>
<button>Download all</button>
<button>Request</button>
<button>Donate</button>
<a href="https://github.com/rektdeckard/phosphor-react">
<button>Github</button>
</a>
</div>
</header>
);

View File

@@ -1,21 +1,50 @@
.grid {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(160px, 1fr));
/* grid-template-columns: repeat(auto-fill, minmax(160px, 1fr)); */
/* display: grid; */
/* grid-template-columns: repeat(auto-fill, 160px);
grid-gap: 10px;
grid-auto-rows: minmax(160px, auto); */
display: flex;
flex-flow: row wrap;
justify-content: space-between;
margin: 16px;
/* min-height: 100vh; */
}
.grid-item {
display: flex;
width: 160px;
height: 160px;
flex-direction: column;
align-items: center;
justify-content: center;
margin: 4px;
border-radius: 8px;
background-color: white;
/* transition: background-color 0.5s ease; */
border-radius: 16px;
user-select: none;
cursor: pointer;
}
.grid-item:hover {
/* background-color: aquamarine; */
/* transition: background-color 0.5s ease; */
.grid-item:focus {
outline: none;
}
.info-box {
display: flex;
margin: 4px;
padding: 16px;
width: 100%;
height: 0px;
border-radius: 16px;
box-shadow: 0 0 0 2px rgb(0, 0, 0);
}
.empty-list {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
padding: 24px;
}

View File

@@ -1,46 +1,73 @@
import React from "react";
import React, { useRef, useEffect } from "react";
import { useRecoilValue } from "recoil";
import { motion } from "framer-motion";
import { motion, useAnimation } from "framer-motion";
import { useWindowSize } from "react-use";
import { filteredQueryResultsSelector } from "../../state/selectors";
import { iconColorAtom, iconSizeAtom, styleQueryAtom } from "../../state/atoms";
import {
iconColorAtom,
iconSizeAtom,
styleQueryAtom,
searchQueryAtom,
} from "../../state/atoms";
import "./IconGrid.css";
import GridItem from "./IconGridItem";
import { WarningTriangle, IconProps } from "phosphor-react";
type IconGridProps = {};
// const variants = {
// open: { opacity: 1, x: 0 },
// closed: { opacity: 0, x: "-100%" },
// }
const whileHover = {
boxShadow: "0 0 0 2px rgb(0, 0, 0)",
// scale: 1.2,
};
const transition = { duration: 0.2 };
const IconGrid: React.FC<IconGridProps> = () => {
const IconGridAnimated: React.FC<IconGridProps> = () => {
const weight = useRecoilValue(styleQueryAtom);
const color = useRecoilValue(iconColorAtom);
const query = useRecoilValue(searchQueryAtom);
const size = useRecoilValue(iconSizeAtom);
const color = useRecoilValue(iconColorAtom);
const iconProps: IconProps = { weight, color, size };
const { width } = useWindowSize();
const spans = Math.floor((width - 32) / 172);
const filteredQueryResults = useRecoilValue(filteredQueryResultsSelector);
const originOffset = useRef({ top: 0, left: 0 });
const controls = useAnimation();
useEffect(() => {
controls.start("visible");
}, [controls, filteredQueryResults]);
if (!filteredQueryResults.length)
return (
<motion.div
className="empty-list"
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
transition={{ duration: 0.2 }}
>
<WarningTriangle size={92} color="darkmagenta" weight="duotone" />
<p>{`No results for '${query}'`}</p>
</motion.div>
);
return (
<motion.div className="grid">
{filteredQueryResults.map(({ name, Icon }) => (
<motion.div
key={name}
className="grid-item"
whileHover={whileHover}
transition={transition}
>
<Icon color={color} size={size} weight={weight} />
<p>{name}</p>
</motion.div>
<motion.div
className="grid"
initial="hidden"
animate={controls}
variants={{}}
>
{filteredQueryResults.map((iconEntry, i) => (
<GridItem
key={i}
index={i}
spans={spans}
{...iconEntry}
{...iconProps}
originOffset={originOffset}
/>
))}
</motion.div>
);
};
export default IconGrid;
export default IconGridAnimated;

View File

@@ -0,0 +1,130 @@
import React, {
useRef,
useLayoutEffect,
useEffect,
MutableRefObject,
} from "react";
import { useRecoilState } from "recoil";
import { motion, AnimatePresence } from "framer-motion";
import { iconPreviewOpenAtom } from "../../state/atoms";
import { IconProps, Icon } from "phosphor-react";
interface IconGridItemProps extends IconProps {
index: number;
name: string;
Icon: Icon;
originOffset: MutableRefObject<{ top: number; left: number }>;
spans: number;
}
const itemVariants = {
hidden: { opacity: 0 },
visible: (delayRef: any) => ({
opacity: 1,
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 [open, setOpen] = useRecoilState(iconPreviewOpenAtom);
const delayRef = useRef<number>(0);
const offset = useRef({ top: 0, left: 0 });
const ref = useRef<any>();
// 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
useLayoutEffect(() => {
const element = ref.current;
if (!element) return;
offset.current = {
top: element.offsetTop,
left: element.offsetLeft,
};
if (index === originIndex) {
originOffset.current = offset.current;
}
}, []);
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;
}, []);
return (
<>
<motion.div
className="grid-item"
ref={ref}
style={{ order: index }}
custom={delayRef}
key={name}
whileHover={whileHover}
whileTap={whileTap}
transition={transition}
variants={itemVariants}
onClick={() =>
setOpen((openName) => (name === openName ? false : name))
}
>
<Icon {...iconProps} />
<p>{name}</p>
</motion.div>
<AnimatePresence initial={false}>
{open === name && <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,46 @@
import React from "react";
import { useRecoilValue } from "recoil";
import { motion } from "framer-motion";
import { filteredQueryResultsSelector } from "../../state/selectors";
import { iconColorAtom, iconSizeAtom, styleQueryAtom } from "../../state/atoms";
import "./IconGrid.css";
type IconGridProps = {};
// const variants = {
// open: { opacity: 1, x: 0 },
// closed: { opacity: 0, x: "-100%" },
// }
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 IconGrid: React.FC<IconGridProps> = () => {
const weight = useRecoilValue(styleQueryAtom);
const color = useRecoilValue(iconColorAtom);
const size = useRecoilValue(iconSizeAtom);
const filteredQueryResults = useRecoilValue(filteredQueryResultsSelector);
return (
<motion.div className="grid">
{filteredQueryResults.map(({ name, Icon }) => (
<motion.div
tabIndex={1}
key={name}
className="grid-item"
whileHover={whileHover}
whileTap={whileTap}
transition={transition}
>
<Icon color={color} size={size} weight={weight} />
<p>{name}</p>
</motion.div>
))}
</motion.div>
);
};
export default IconGrid;

View File

@@ -2,7 +2,7 @@
position: relative;
display: flex;
align-items: center;
width: 250px;
flex: 2;
margin: 4px;
padding: 8px 16px;
border: 1px solid black;

View File

@@ -16,11 +16,9 @@ const SearchInput: React.FC<SearchInputProps> = () => {
return (
<div className="search-bar">
<Search />
<label htmlFor="search-input" hidden>
Search for an icon
</label>
<input
id="search-input"
aria-label="Search for an icon"
type="text"
value={query}
placeholder="Search for an icon"

View File

@@ -2,7 +2,7 @@
position: relative;
display: flex;
align-items: center;
width: 250px;
flex: 2;
margin: 4px;
padding: 8px 16px;
border: 1px solid black;
@@ -10,7 +10,7 @@
background-color: white;
font-family: sans-serif;
font-size: 13.333px;
height: 20px;
height: 17px;
}
.size-bar:focus-within {

View File

@@ -15,11 +15,9 @@ const StyleInput: React.FC<StyleInputProps> = () => {
return (
<div>
<label htmlFor="style-input" hidden>
Icon Size
</label>
<select
id="style-input"
aria-label="Icon Style"
value={style?.toString()}
onChange={handleStyleChange}
>

View File

@@ -1,11 +1,14 @@
.toolbar {
position: sticky;
top: 0px;
padding: 8px;
background-color: #f2f2f2;
border: 1px 0px 1px solid black;
z-index: 2px;
padding: 4px;
/* background-color: #f2f2f2; */
background-color: #e2e2e2;
border-top: 1px solid black;
border-bottom: 1px solid black;
display: flex;
z-index: 1;
flex-flow: wrap;
justify-content: center;
align-items: center;
}

View File

@@ -27,3 +27,8 @@ export const iconColorAtom = atom<string>({
key: "iconColorAtom",
default: "#000000",
});
export const iconPreviewOpenAtom = atom<string | false>({
key: "iconPreviewOpenAtom",
default: false,
});