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

@@ -1,44 +1,64 @@
This project was bootstrapped with [Create React App](https://github.com/facebook/create-react-app). # Phosphor Icons
## Available Scripts Phosphor is a kickass and dead-simple set of open-source icons for web and digital media. We aim to provide variety, consistency, and above all, ease-of-use for digital content creators of all kinds.
In the project directory, you can run: ## For developers
### `yarn start` Phosphor is available as an icon font and a React package, which can be sourced from NPM or from a CDN.
Runs the app in the development mode.<br /> ### Vanilla JS
Open [http://localhost:3000](http://localhost:3000) to view it in the browser.
The page will reload if you make edits.<br /> - **This seems familiar...** Using Phosphor in your web project might seem familiar. We use a similar approach as many other icon sets out there, providing icons as a webfont that uses Unicode's Private Use Area character codes to map normally non-rendering characters to icons. But you don't need to know that. All you need to do is source the stylesheet, and drop in an icon:
You will also see any lint errors in the console.
### `yarn test` ```html
<!DOCTYPE html>
<html>
<head>
<script src="https://unpkg.com/phosphor-web@latest"></script>
</head>
<body>
<i class="ph-smiley"></i>
<i class="ph-heart-fill" style="color: hotpink"></i>
<i class="ph-cube-duotone"></i>
</body>
</html>
```
Launches the test runner in the interactive watch mode.<br /> - **Whatchacallit?** We use a straightforward and semantic naming scheme that may mean only changing a few letters when switching from other icon sets. But don't switch on our account, there are some excellent sets out there!
See the section about [running tests](https://facebook.github.io/create-react-app/docs/running-tests) for more information. - **That's it?** Yep. That's it.
### `yarn build` ### React
Builds the app for production to the `build` folder.<br /> - **Flex or flow** Phosphor's intuitive but powerful API can style the `color`, `size`, and `weight` of an icon with a few keystrokes, or directly manipulate the SVG at runtime through render props to do some amazing things! Check out some examples on [Github](https://github.com/rektdeckard/phosphor-react).
It correctly bundles React in production mode and optimizes the build for the best performance.
The build is minified and the filenames include the hashes.<br /> ```jsx
Your app is ready to be deployed! import React from "react";
import ReactDOM from "react-dom";
import { Smiley, Heart, Horse } from "phosphor-react";
See the section about [deployment](https://facebook.github.io/create-react-app/docs/deployment) for more information. const App = () => {
return (
<>
<Smiley />
<Heart size={32} color="hotpink" weight="fill" />
<Horse weight="duotone" />
</>
);
};
### `yarn eject` ReactDOM.render(<App />, document.getElementById("root"));
```
**Note: this is a one-way operation. Once you `eject`, you cant go back!** - **Light as a Feather** Supports tree-shaking, so your bundle only includes code for the icons you use.
- **Familiar** Icon Components are a thin wrapper around SVG elements, so feel free to add your own inline `style` objects, `onClick` handler functions, and a multitude of other props you're used to using on React Elements.
If you arent satisfied with the build tool and configuration choices, you can `eject` at any time. This command will remove the single build dependency from your project. ## For designers
Instead, it will copy all the configuration files and the transitive dependencies (webpack, Babel, ESLint, etc) right into your project so you have full control over them. All of the commands except `eject` will still work, but they will point to the copied scripts so you can tweak them. At this point youre on your own. ### Raw Assets
- **SVGs** Grab our individual icon SVGs, in both minified and original formats retaining design-time detail.
- **Icon Font** Use the icons as you would text, in applications where full-fledged graphical elements are undesirable.
You dont have to ever use `eject`. The curated feature set is suitable for small and middle deployments, and you shouldnt feel obligated to use this feature. However we understand that this tool wouldnt be useful if you couldnt customize it when you are ready for it. ### Source Files
- **Sketch**
## Learn More - **Illustrator**
- **Figma**
You can learn more in the [Create React App documentation](https://facebook.github.io/create-react-app/docs/getting-started).
To learn React, check out the [React documentation](https://reactjs.org/).

View File

@@ -18,6 +18,7 @@
"react-dom": "^16.13.1", "react-dom": "^16.13.1",
"react-list": "^0.8.15", "react-list": "^0.8.15",
"react-scripts": "3.4.1", "react-scripts": "3.4.1",
"react-use": "^15.3.2",
"react-virtual": "^2.2.1", "react-virtual": "^2.2.1",
"react-virtualized": "^9.21.2", "react-virtualized": "^9.21.2",
"recoil": "^0.0.10", "recoil": "^0.0.10",

View File

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

View File

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

View File

@@ -1,21 +1,50 @@
.grid { .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; */ /* min-height: 100vh; */
} }
.grid-item { .grid-item {
display: flex; display: flex;
width: 160px;
height: 160px;
flex-direction: column; flex-direction: column;
align-items: center; align-items: center;
justify-content: center; justify-content: center;
margin: 4px; margin: 4px;
border-radius: 8px; border-radius: 16px;
background-color: white; user-select: none;
/* transition: background-color 0.5s ease; */ cursor: pointer;
} }
.grid-item:hover { .grid-item:focus {
/* background-color: aquamarine; */ outline: none;
/* transition: background-color 0.5s ease; */ }
.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 { 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 { filteredQueryResultsSelector } from "../../state/selectors";
import { iconColorAtom, iconSizeAtom, styleQueryAtom } from "../../state/atoms"; import {
iconColorAtom,
iconSizeAtom,
styleQueryAtom,
searchQueryAtom,
} from "../../state/atoms";
import "./IconGrid.css"; import "./IconGrid.css";
import GridItem from "./IconGridItem";
import { WarningTriangle, IconProps } from "phosphor-react";
type IconGridProps = {}; type IconGridProps = {};
// const variants = { const IconGridAnimated: React.FC<IconGridProps> = () => {
// 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 weight = useRecoilValue(styleQueryAtom); const weight = useRecoilValue(styleQueryAtom);
const color = useRecoilValue(iconColorAtom); const query = useRecoilValue(searchQueryAtom);
const size = useRecoilValue(iconSizeAtom); 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 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 ( return (
<motion.div className="grid"> <motion.div
{filteredQueryResults.map(({ name, Icon }) => ( className="grid"
<motion.div initial="hidden"
key={name} animate={controls}
className="grid-item" variants={{}}
whileHover={whileHover} >
transition={transition} {filteredQueryResults.map((iconEntry, i) => (
> <GridItem
<Icon color={color} size={size} weight={weight} /> key={i}
<p>{name}</p> index={i}
</motion.div> spans={spans}
{...iconEntry}
{...iconProps}
originOffset={originOffset}
/>
))} ))}
</motion.div> </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; position: relative;
display: flex; display: flex;
align-items: center; align-items: center;
width: 250px; flex: 2;
margin: 4px; margin: 4px;
padding: 8px 16px; padding: 8px 16px;
border: 1px solid black; border: 1px solid black;

View File

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

View File

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

View File

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

View File

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

View File

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