Massive interactivity updates to all components
This commit is contained in:
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
|
||||
130
src/components/IconGrid/IconGridItem.tsx
Normal file
130
src/components/IconGrid/IconGridItem.tsx
Normal 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;
|
||||
46
src/components/IconGrid/IconGridStatic.tsx
Normal file
46
src/components/IconGrid/IconGridStatic.tsx
Normal 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;
|
||||
Reference in New Issue
Block a user