30 Commits

Author SHA1 Message Date
rektdeckard
4d7f5ea100 chore(lib): Bump version to v1.4.0 2021-12-23 03:55:37 -05:00
rektdeckard
3cdcdd4e0d chore(grid): add some types 2021-12-22 01:25:58 -05:00
rektdeckard
c090531800 feat(icons): update icons and tags to 1.4 2021-12-22 01:25:31 -05:00
rektdeckard
887617e523 feat(bin): update fetch script to take CLI args 2021-12-12 00:27:14 -05:00
rektdeckard
e242bcc660 feat(bin): add icon db fetch script 2021-12-05 22:58:42 -05:00
rektdeckard
22b69c3ae6 chore(links): ignore pointer events on link underlines 2021-12-03 19:58:04 -05:00
rektdeckard
e4b99d2ca9 chore(app): better ux for settings actions 2021-11-29 21:14:17 -05:00
rektdeckard
14d8c9d0e7 feat(app): add persistence of settings across sessions 2021-11-26 22:03:54 -05:00
rektdeckard
cdcf38466e feat(ui): add callouts for new and updated icons 2021-11-26 22:03:01 -05:00
rektdeckard
f256109ba4 chore(deps): upgrade recoil 2021-11-26 22:01:20 -05:00
Tobias Fried
bcff9fecb3 chore(readme): typo 2021-10-20 20:21:48 -04:00
Tobias Fried
a218b632ba chore(readme): add community projects 2021-10-20 20:17:13 -04:00
rektdeckard
56dd2ba514 feat(external): Add Sketch plugin link 2021-07-09 23:03:32 -04:00
rektdeckard
f5089e1c60 chore(readme): Update icon count 2021-07-08 02:48:43 -04:00
rektdeckard
77d93e4038 chore(readme): Revisions for v1.3.1 2021-07-08 02:09:33 -04:00
rektdeckard
b6e2ae7da5 feat(deps): Update to phosphor-react@1.3.1 and add icon entries 2021-07-05 22:46:47 -04:00
rektdeckard
a885931831 app: Mobile grid improvements 2021-06-20 19:53:53 -04:00
rektdeckard
2eb51f7ca7 icons: yet more tagging and updates for v1.3.0 2021-06-14 00:53:10 -04:00
rektdeckard
7e1bd3d18e icons: more tagging 2021-06-13 12:47:11 -04:00
rektdeckard
94e5d9b305 chore: Update snippets and unused code 2021-06-13 12:46:45 -04:00
rektdeckard
02e70848b1 icons: tag additions for v1.3.0 2021-05-31 16:43:46 -04:00
rektdeckard
b8eac52689 App: browser compat updates 2021-05-30 23:31:26 -04:00
rektdeckard
73b66e2e86 App: add helper hooks 2021-05-30 23:17:33 -04:00
rektdeckard
0e50efb5ea icons: more tags 2021-05-29 23:11:31 -04:00
rektdeckard
dc6764e387 DetailsPanel: smooth panel transitions to eliminate bounce 2021-03-13 12:29:57 -05:00
Tobias Fried
2ba5ac332b Merge pull request #29 from phosphor-icons/phosphor-react-refactor
Dependencipalooza
2021-03-09 00:10:45 -05:00
rektdeckard
b9a0b93067 meta: Bump site version to match phosphor dep 2021-03-08 23:59:38 -05:00
rektdeckard
6596bce68a SearchInput: More idiomatic shortcuts 2021-03-08 23:58:29 -05:00
rektdeckard
6d74c9f719 DetailsPanel: Avoid framer-motion style value bug 2021-03-08 23:57:42 -05:00
rektdeckard
02525cabb5 deps: Bump phopshor-react, framer-motion, recoil, tinycolor 2021-03-08 23:57:09 -05:00
34 changed files with 3408 additions and 410 deletions

View File

@@ -21,8 +21,8 @@
"{",
"\tname: \"${1:name}\",",
"\tcategories: [IconCategory${2:categories}],",
"\ttags: [${3:tags}],",
"\tIcon: ${4:icon},",
"\ttags: [\"*new*\", ${3:tags}],",
"\tIcon: Icons.${4:icon},",
"},"
],
"description": "Create an IconEntry for phosphor-home"

View File

@@ -4,7 +4,7 @@
Phosphor is a flexible icon family for interfaces, diagrams, presentations — whatever, really.
- 772 icons and counting
- 1047 icons and counting
- 6 weights: **Thin**, **Light**, **Regular**, **Bold**, **Fill**, and **Duotone**
- Designed at 16 x 16px to read well small and scale up big
- Raw stroke information retained to fine-tune the style
@@ -88,7 +88,7 @@ ReactDOM.render(<App />, document.getElementById("root"));
> **Note:** Due to possible namespace collisions with built-in HTML elements, compononent names in the Vue library are prefixed with `Ph`, but otherwise follow the same naming conventions. Both Pascal and kebab-case conventions can be used in templates.
## Related Projects
## Our Related Projects
- [phosphor-react](https://github.com/phosphor-icons/phosphor-react) ▲ Phosphor icon component library for React
- [phosphor-vue](https://github.com/phosphor-icons/phosphor-vue) ▲ Phosphor icon component library for Vue
@@ -96,6 +96,16 @@ ReactDOM.render(<App />, document.getElementById("root"));
- [phosphor-flutter](https://github.com/phosphor-icons/phosphor-flutter) ▲ Phosphor IconData library for Flutter
- [phosphor-webcomponents](https://github.com/phosphor-icons/phosphor-webcomponents) ▲ Phosphor icons as Web Components
- [phosphor-figma](https://github.com/phosphor-icons/phosphor-figma) ▲ Phosphor icons Figma plugin
- [phosphor-sketch](https://github.com/phosphor-icons/phosphor-sketch) ▲ Phosphor icons Sketch plugin
## Community Projects
- [phosphor-react-native](https://github.com/duongdev/phosphor-react-native) ▲ Phosphor icon component library for React Native
- [phosphor-svelte](https://github.com/haruaki07/phosphor-svelte) ▲ Phosphor icons for Svelte apps
- [phosphor-r](https://github.com/dreamRs/phosphoricons) ▲ Phosphor icon wrapper for R documents and applications
- [blade-phosphor-icons](https://github.com/codeat3/blade-phosphor-icons) ▲ Phosphor icons in your Laravel Blade views
If you've made a port of Phosphor and you want to see it here, just open a PR [here](https://github.com/phosphor-icons/phosphor-home)!
## License

93
bin/fetch.js Normal file
View File

@@ -0,0 +1,93 @@
#!/usr/bin/env node
"use strict";
const fs = require("fs/promises");
const path = require("path");
const axios = require("axios");
const chalk = require("chalk");
const { Command } = require("commander");
const { version } = require("../package.json");
const ICON_API_URL = "https://api.phosphoricons.com";
async function main() {
const program = new Command();
program
.version(version)
.option(
"-r --release <version>",
"Fetch icons from Phosphor API and compile to internal data structure"
)
.option("-p --published", "Published status of icons")
.option("-P, --no-published", "Published status of icons")
.option("-q --query <text>", "Fulltext search term")
.option("-n --name <name>", "Exact icon namee match");
program.parse(process.argv);
const params = new URLSearchParams(Object.entries(program.opts())).toString();
try {
const res = await axios.get(`${ICON_API_URL}?${params}`);
if (res.data) {
let fileString = `\
import * as Icons from "phosphor-react";
import { IconEntry, IconCategory } from ".";
export const icons: ReadonlyArray<IconEntry> = [
`;
res.data.icons.forEach((icon) => {
let categories = "[";
icon.searchCategories?.forEach((c) => {
categories += `IconCategory.${c.toUpperCase()},`;
});
categories += "]";
fileString += `\
{
name: "${icon.name}",
categories: ${categories},
tags: ${JSON.stringify(["*new*", ...icon.tags])},
Icon: Icons.${icon.name
.split("-")
.map((substr) => substr.replace(/^\w/, (c) => c.toUpperCase()))
.join("")},
},
`;
console.log(`${chalk.inverse.green(" DONE ")} ${icon.name}`);
});
fileString += `
];
if (process.env.NODE_ENV === "development") {
console.log(\`\${icons.length} icons\`);
}
export const iconCount = (icons.length * 6)
.toString()
.replace(/\B(?=(\d{3})+(?!\d))/g, ",");
`;
try {
await fs.writeFile(
path.join(__dirname, "../src/lib/new.ts"),
fileString
);
console.log(
`${chalk.green(" DONE ")} ${res.data.icons.length} icons ingested`
);
} catch (e) {
console.error(`${chalk.inverse.red(" FAIL ")} Could not write file`);
}
} else {
console.error(`${chalk.inverse.red(" FAIL ")} No data`);
}
} catch (e) {
console.error(e);
process.exit(-1);
}
}
main();

View File

@@ -1,6 +1,6 @@
{
"name": "phosphor-home",
"version": "1.2.0",
"version": "1.4.0",
"license": "MIT",
"homepage": "https://phosphoricons.com",
"author": {
@@ -20,11 +20,20 @@
],
"repository": "github:phosphor-icons/phosphor-home",
"private": true,
"scripts": {
"analyze": "source-map-explorer 'build/static/js/*.js'",
"start": "react-scripts start",
"build": "react-scripts build",
"test": "react-scripts test",
"eject": "react-scripts eject",
"fetch": "node ./bin/fetch.js",
"format": "prettier --write \"./src/**/*.{js,jsx,ts,tsx,json,vue}\""
},
"dependencies": {
"file-saver": "^2.0.2",
"framer-motion": "^2.1.0",
"framer-motion": "^3.10.0",
"fuse.js": "^6.4.1",
"phosphor-react": "^1.2.0",
"phosphor-react": "^1.4.0",
"react": "^17.0.1",
"react-dom": "^17.0.1",
"react-dropdown-select": "^4.4.2",
@@ -32,19 +41,25 @@
"react-hotkeys-hook": "^3.2.1",
"react-scripts": "3.4.1",
"react-use": "^15.3.2",
"recoil": "^0.1.2",
"recoil": "^0.5.2",
"svg2png-converter": "^1.0.0",
"tinycolor2": "^1.4.1"
"tinycolor2": "^1.4.2"
},
"scripts": {
"analyze": "source-map-explorer 'build/static/js/*.js'",
"start": "react-scripts start",
"build": "react-scripts build",
"test": "react-scripts test",
"eject": "react-scripts eject",
"predeploy": "npm run build",
"deploy": "gh-pages -d build",
"format": "prettier --write \"./src/**/*.{js,jsx,ts,tsx,json,vue}\""
"devDependencies": {
"@testing-library/jest-dom": "^4.2.4",
"@testing-library/react": "^9.3.2",
"@testing-library/user-event": "^7.1.2",
"@types/file-saver": "^2.0.1",
"@types/jest": "^24.0.0",
"@types/node": "^12.0.0",
"@types/react": "^16.9.46",
"@types/react-dom": "^16.9.8",
"@types/react-virtualized": "^9.21.10",
"@types/tinycolor2": "^1.4.2",
"axios": "^0.24.0",
"chalk": "^4",
"commander": "^8.3.0",
"typescript": "^3.9.6"
},
"eslintConfig": {
"extends": "react-app"
@@ -60,19 +75,5 @@
"last 1 firefox version",
"last 1 safari version"
]
},
"devDependencies": {
"@testing-library/jest-dom": "^4.2.4",
"@testing-library/react": "^9.3.2",
"@testing-library/user-event": "^7.1.2",
"@types/file-saver": "^2.0.1",
"@types/jest": "^24.0.0",
"@types/node": "^12.0.0",
"@types/react": "^16.9.46",
"@types/react-dom": "^16.9.8",
"@types/react-list": "^0.8.5",
"@types/react-virtualized": "^9.21.10",
"@types/tinycolor2": "^1.4.2",
"typescript": "^3.9.6"
}
}

View File

@@ -18,6 +18,13 @@
"sizes": "512x512"
}
],
"permissions": [
"http://*/*",
"https://*/*",
"clipboardRead",
"clipboardWrite",
"storage"
],
"start_url": ".",
"display": "standalone",
"theme_color": "#35313D",

View File

@@ -15,10 +15,10 @@ h2 {
}
img {
user-select: none;
-moz-user-select: none;
-webkit-user-select: none;
-webkit-user-drag: none;
user-select: none;
}
pre,
@@ -65,9 +65,10 @@ button.main-button {
transform: translate(0, 0);
transition: all 0.2s ease;
cursor: pointer;
user-select: none;
-moz-user-select: none;
-webkit-user-select: none;
-webkit-user-drag: none;
user-select: none;
margin: 0 24px 24px 0;
}
@@ -108,8 +109,22 @@ a.main-link:after {
width: 100%;
border-bottom: 1px solid black;
transition: 0.2s;
pointer-events: none;
}
a.main-link:hover:after {
width: 0%;
}
.badge.new {
color: #ff6e60;
}
.badge.updated {
color: #397fff;
}
.badge {
font-size: 24px;
line-height: 0.5em;
}

View File

@@ -8,12 +8,14 @@ import Footer from "../Footer/Footer";
import ErrorBoundary from "../ErrorBoundary/ErrorBoundary";
import Notice from "../Notice/Notice";
import useIconParameters from "../../hooks/useIconParameters";
import usePersistSettings from "../../hooks/usePersistSettings";
const errorFallback = <Notice message="Search error" />;
const waitingFallback = <Notice type="none" message="" />;
const App: React.FC<any> = () => {
useIconParameters();
usePersistSettings();
return (
<React.StrictMode>

View File

@@ -16,10 +16,11 @@
left: 50%;
-ms-transform: translate(-50%, -50%);
transform: translate(-50%, -50%);
user-select: none;
-moz-user-select: none;
-webkit-user-select: none;
user-select: none;
pointer-events: none;
text-transform: uppercase;
}
input.color-input {

View File

@@ -48,9 +48,9 @@ footer .links {
.illustrations-footer {
display: none;
user-select: none;
-moz-user-select: none;
-webkit-user-select: none;
user-select: none;
pointer-events: none;
}

View File

@@ -60,7 +60,7 @@ header {
top: -158px;
}
#billiard-ball {
.billiard-ball {
position: absolute;
left: 132px;
top: -98px;
@@ -72,13 +72,13 @@ header {
top: 152px;
}
#warning {
.warning {
position: absolute;
left: 394px;
top: -304px;
}
#tablet {
.tablet {
position: absolute;
left: 672px;
top: -900px;
@@ -94,18 +94,18 @@ header {
height: 612px;
}
#cutting-mat {
.cutting-mat {
position: absolute;
left: 96px;
}
#receipt {
.receipt {
position: absolute;
left: -36px;
top: 190px;
}
#calculator {
.calculator {
position: absolute;
left: 632px;
top: 170px;
@@ -131,7 +131,7 @@ header {
top: -158px;
}
#billiard-ball {
.billiard-ball {
position: absolute;
left: 900px;
top: 400px;
@@ -148,30 +148,30 @@ header {
top: 694px;
}
#warning {
.warning {
position: absolute;
left: 1170px;
top: 400px;
}
#tablet {
.tablet {
position: absolute;
left: 578px;
top: -900px;
}
#cutting-mat {
.cutting-mat {
position: absolute;
left: 120px;
}
#receipt {
.receipt {
position: absolute;
left: -16px;
top: 190px;
}
#calculator {
.calculator {
position: absolute;
left: 924px;
top: 114px;

View File

@@ -41,22 +41,22 @@ const Header: React.FC<HeaderProps> = () => {
<img src={markerPurple} id="marker-purple" alt="" />
<img src={paperclips} id="paperclips" alt="" />
<img src={paperclipsThree} id="paperclips-three" alt="" />
<img id="tablet" src={tabletSpec} alt="" />
<img id="tablet" className="inspectable xray" src={tablet} alt="" />
<img id="billiard-ball" src={billiardBallSpec} alt="" />
<img className="tablet" src={tabletSpec} alt="" />
<img className="tablet inspectable xray" src={tablet} alt="" />
<img className="billiard-ball" src={billiardBallSpec} alt="" />
<img
id="billiard-ball"
className="inspectable xray"
className="billiard-ball inspectable xray"
src={billiardBall}
alt=""
/>
<img id="warning" src={warningSpec} alt="" />
<img id="warning" className="inspectable xray" src={warning} alt="" />
<img className="warning" src={warningSpec} alt="" />
<img className="warning inspectable xray" src={warning} alt="" />
</div>
<div className="intro">
<h2>
Phosphor is a flexible icon family for interfaces, diagrams, presentations  
Phosphor is a flexible icon family for interfaces, diagrams,
presentations  
<wbr />
whatever, really.
</h2>
@@ -73,19 +73,17 @@ const Header: React.FC<HeaderProps> = () => {
<Links />
</div>
<div className="illustrations-bottom">
<img id="cutting-mat" src={cuttingMatSpec} alt="" />
<img className="cutting-mat" src={cuttingMatSpec} alt="" />
<img
id="cutting-mat"
className="inspectable xray"
className="cutting-mat inspectable xray"
src={cuttingMat}
alt=""
/>
<img id="receipt" src={receiptSpec} alt="" />
<img id="receipt" className="inspectable xray" src={receipt} alt="" />
<img id="calculator" src={calculatorSpec} alt="" />
<img className="receipt" src={receiptSpec} alt="" />
<img className="receipt inspectable xray" src={receipt} alt="" />
<img className="calculator" src={calculatorSpec} alt="" />
<img
id="calculator"
className="inspectable xray"
className="calculator inspectable xray"
src={calculator}
alt=""
/>

View File

@@ -20,22 +20,22 @@ const panelVariants = {
open: {
opacity: 1,
height: "100%",
marginTop: 4,
marginBottom: 4,
// transition: { stiffness: 600, damping: 32, duration: 0.2 },
marginTop: "4px",
marginBottom: "4px",
transition: { type: "tween", duration: 0.1 },
},
collapsed: {
opacity: 0,
height: 0,
marginTop: 0,
marginBottom: 0,
// transition: { stiffness: 600, damping: 32, duration: 0.2 },
height: "0px",
marginTop: "0px",
marginBottom: "0px",
transition: { type: "tween", duration: 0.1 },
},
};
const contentVariants = {
open: { opacity: 1, transition: { duration: 0.2 } },
collapsed: { opacity: 0, transition: { duration: 0.1 } },
open: { opacity: 1, transition: { duration: 0.2, delay: 0.1 } },
collapsed: { opacity: 0, transition: { duration: 0 } },
};
const buttonColor = "#35313D";

View File

@@ -22,9 +22,9 @@
justify-content: center;
margin: 4px;
border-radius: 16px;
user-select: none;
-moz-user-select: none;
-webkit-user-select: none;
user-select: none;
cursor: pointer;
/* transition: background-color 100ms ease; */
}
@@ -46,6 +46,24 @@
text-align: center;
}
@media screen and (max-width: 536px) {
.grid-container {
padding: 32px 8px;
}
.grid-item {
width: 108px;
height: unset;
padding: 4px 0;
justify-content: flex-start;
border: 2px solid transparent;
}
.grid-item p {
padding: 0 4px;
}
}
.info-box {
position: relative;
display: flex;
@@ -64,6 +82,10 @@
.icon-usage {
padding-left: 10% !important;
}
.snippet pre {
padding: 12px 8px 12px 20px;
}
}
.icon-preview {
@@ -95,9 +117,9 @@
align-items: center;
text-overflow: ellipsis;
color: black;
user-select: all;
-moz-user-select: all;
-webkit-user-select: all;
user-select: all;
}
.snippet pre:focus {
@@ -106,9 +128,9 @@
@keyframes select {
to {
user-select: text;
-moz-user-select: text;
-webkit-user-select: text;
user-select: text;
}
}

View File

@@ -14,7 +14,15 @@ import TagCloud from "./TagCloud";
import Notice from "../Notice/Notice";
import "./IconGrid.css";
const defaultSearchTags = ["*new*", "communication", "editor", "emoji", "maps", "weather"];
const defaultSearchTags = [
"*new*",
"*updated*",
"communication",
"editor",
"emoji",
"maps",
"weather",
];
type IconGridProps = {};

View File

@@ -25,7 +25,7 @@ const delayPerPixel = 0.0004;
const itemVariants = {
hidden: { opacity: 0 },
visible: (delayRef: any) => ({
visible: (delayRef: MutableRefObject<number>) => ({
opacity: 1,
transition: { delay: delayRef.current },
}),
@@ -36,6 +36,8 @@ const IconGridItem: React.FC<IconGridItemProps> = (props) => {
const { name, Icon } = entry;
const [open, setOpen] = useRecoilState(iconPreviewOpenAtom);
const isOpen = open === name;
const isNew = entry.tags.includes("*new*");
const isUpdated = entry.tags.includes("*updated*");
const delayRef = useRef<number>(0);
const offset = useRef({ top: 0, left: 0 });
const ref = useRef<any>();
@@ -84,11 +86,15 @@ const IconGridItem: React.FC<IconGridItemProps> = (props) => {
onClick={handleOpen}
>
<Icon />
<p>{name}</p>
<p>
{name}
{isNew && <span className="badge new"></span>}
{isUpdated && <span className="badge updated"></span>}
</p>
</motion.div>
<AnimatePresence initial={false}>
{isOpen && <DetailsPanel {...props} />}
</AnimatePresence>
<AnimatePresence initial={false}>
{isOpen && <DetailsPanel {...props} />}
</AnimatePresence>
</>
);
};

View File

@@ -29,4 +29,4 @@ button.tag-button:focus {
.dark {
color: white;
}
}

View File

@@ -29,6 +29,8 @@ const TagCloud: React.FC<TagCloudProps> = ({ name, tags, isDark }) => {
onClick={() => void handleTagClick(tag)}
>
<code className={`${isDark ? "dark" : ""}`}>{tag}</code>
{tag === "*new*" && <span className="badge new"></span>}
{tag === "*updated*" && <span className="badge updated"></span>}
</button>
))}
</div>

View File

@@ -23,56 +23,39 @@ const Links: React.FC<LinksProps> = () => {
Download all ({iconCount})
</OutboundLink>
</div>
<div>
<div>
<ArrowElbowDownRight size={24} />
<span>
<OutboundLink
className="nav-link"
to="https://www.figma.com/community/file/903830135544202908/Phosphor-Icons"
eventLabel="Figma library"
>
Figma library
</OutboundLink>
{" / "}
<OutboundLink
className="nav-link"
to="https://www.figma.com/community/plugin/898620911119764089/Phosphor-Icons"
eventLabel="Figma plugin"
>
plugin
</OutboundLink>
<OutboundLink
className="nav-link"
to="https://www.figma.com/community/file/903830135544202908/Phosphor-Icons"
eventLabel="Figma library"
>
Figma library
</OutboundLink>
{" / "}
<OutboundLink
className="nav-link"
to="https://www.figma.com/community/plugin/898620911119764089/Phosphor-Icons"
eventLabel="Figma plugin"
>
plugin
</OutboundLink>
</span>
</div>
{/* <div>
<ArrowElbowDownRight size={24} />
<OutboundLink
className="nav-link"
to="https://www.figma.com/file/xMCDSp5g0g7Fw8aMyAdVVr/Phosphor-Icon-Library-0.6.0"
eventLabel="Figma library"
>
Figma library
</OutboundLink>
</div>
<div>
<ArrowElbowDownRight size={24} />
<OutboundLink
className="nav-link"
to="https://www.figma.com/community/plugin/892854133443228626/Phosphor-Icons"
eventLabel="Figma plugin"
to="https://phosphoricons.com/assets/phosphor-icons.sketchplugin.zip"
eventLabel="Download sketch plugin"
download
type="application/zip"
>
Figma plugin
Sketch plugin
</OutboundLink>
</div> */}
<div>
<ArrowElbowDownRight size={24} />
<a
className="nav-link"
href="https://github.com/phosphor-icons/phosphor-home/issues"
>
Request an icon
</a>
</div>
{/* <div>
<div>
<ArrowElbowDownRight size={24} />
<span>
<a className="nav-link" href="https://paypal.me/minoraxis">
@@ -83,8 +66,8 @@ const Links: React.FC<LinksProps> = () => {
Patreon
</a>
</span>
</div> */}
<div>
</div>
{/* <div>
<ArrowElbowDownRight size={24} />
<a className="nav-link" href="https://paypal.me/minoraxis">
Donate on PayPal
@@ -96,6 +79,7 @@ const Links: React.FC<LinksProps> = () => {
Support us on Patreon
</a>
</div>
*/}
<div>
<ArrowElbowDownRight size={24} />
<a
@@ -105,6 +89,15 @@ const Links: React.FC<LinksProps> = () => {
GitHub
</a>
</div>
<div>
<ArrowElbowDownRight size={24} />
<a
className="nav-link"
href="https://github.com/phosphor-icons/phosphor-home/issues"
>
Request an icon
</a>
</div>
</div>
);
};

View File

@@ -56,6 +56,8 @@
display: inline-flex;
align-items: center;
justify-content: flex-end;
-moz-user-select: none;
-webkit-user-select: none;
user-select: none;
min-width: 42px;
font-size: 12px;

View File

@@ -81,7 +81,7 @@ const SearchInput: React.FC<SearchInputProps> = () => {
key === "Enter" && currentTarget.blur()
}
/>
{!value && !isMobile && <Keys>{isApple ? <Command /> : "Ctrl"} + K</Keys>}
{!value && !isMobile && <Keys>{isApple ? <Command /> : "Ctrl + "}K</Keys>}
{value ? (
isReady() ? (
<X className="clear-icon" size={18} onClick={handleCancelSearch} />

View File

@@ -0,0 +1,21 @@
button.action-button {
background-color: rgba(255, 255, 255, 0.05);
color: white;
padding: 8px;
border-radius: 8px;
cursor: pointer;
}
button.action-button:hover {
background-color: rgba(255, 255, 255, 0.1);
}
button.action-button:active {
background-color: rgba(255, 255, 255, 0.2);
}
@media screen and (max-width: 558px) {
.action-button {
display: none;
}
}

View File

@@ -0,0 +1,57 @@
import React from "react";
import { ArrowCounterClockwise, CheckCircle, Link } from "phosphor-react";
import { useRecoilValue, useResetRecoilState } from "recoil";
import { iconWeightAtom, iconSizeAtom, iconColorAtom } from "../../state/atoms";
import "./SettingsActions.css";
import useTransientState from "../../hooks/useTransientState";
import { resetSettingsSelector } from "../../state/selectors";
const SettingsActions: React.FC = () => {
const weight = useRecoilValue(iconWeightAtom);
const size = useRecoilValue(iconSizeAtom);
const color = useRecoilValue(iconColorAtom);
const reset = useResetRecoilState(resetSettingsSelector);
const [copied, setCopied] = useTransientState<boolean>(false, 2000);
const copyDeepLinkToClipboard = () => {
const paramString = new URLSearchParams([
["weight", weight.toString()],
["size", size.toString()],
["color", color.replace("#", "")],
]).toString();
void navigator.clipboard
?.writeText(`${window.location.host}?${paramString}`)
.then(() => {
setCopied(true);
})
.catch(() => {
alert("Clipboard permissions must be enabled to copy links!");
});
};
return (
<>
<button
className="action-button"
title="Restore default settings"
onClick={reset}
>
<ArrowCounterClockwise size={24} />
</button>
<button
className="action-button"
title="Copy URL for current settings"
onClick={copyDeepLinkToClipboard}
>
{copied ? (
<CheckCircle size={24} color="#1FA647" weight="fill" />
) : (
<Link size={24} />
)}
</button>
</>
);
};
export default SettingsActions;

View File

@@ -1,4 +1,5 @@
menu.toolbar {
nav.toolbar {
position: -webkit-sticky;
position: sticky;
top: -1px;
padding: 0;

View File

@@ -5,19 +5,21 @@ import StyleInput from "../StyleInput/StyleInput";
import SearchInput from "../SearchInput/SearchInput";
import SizeInput from "../SizeInput/SizeInput";
import ColorInput from "../ColorInput/ColorInput";
import SettingsActions from "../SettingsActions/SettingsActions";
type ToolbarProps = {};
const Toolbar: React.FC<ToolbarProps> = () => {
return (
<menu className="toolbar" id="toolbar">
<nav className="toolbar" id="toolbar">
<div className="toolbar-contents">
<StyleInput />
<SearchInput />
<SizeInput />
<ColorInput />
<SettingsActions />
</div>
</menu>
</nav>
);
};

16
src/hooks/useDebounce.ts Normal file
View File

@@ -0,0 +1,16 @@
import { DependencyList, useEffect } from "react";
import useTimeoutFn from "./useTimeoutFn";
export type UseDebounceReturn = [() => boolean | null, () => void];
export default function useDebounce(
fn: Function,
ms: number = 0,
deps: DependencyList = []
): UseDebounceReturn {
const [isReady, cancel, reset] = useTimeoutFn(fn, ms);
useEffect(reset, deps);
return [isReady, cancel];
}

View File

@@ -1,13 +1,18 @@
import { useWindowSize } from "react-use";
const MOBILE_BREAKPOINT = 536;
const GRID_PADDING = 32; // .grid-container { padding }
const TOOLBAR_WIDTH = 17; // IS THIS BROWSER-SPECIFIC?
const MAX_GRID_WIDTH = 1120; // .grid { max-width }
const ITEM_WIDTH = 168; // .grid-item { width; height; margin }
const ITEM_WIDTH_MOBILE = 108; // .grid-item { width; height; margin }
export default (): number => {
const { width } = useWindowSize();
const itemWidth = width <= MOBILE_BREAKPOINT ? ITEM_WIDTH_MOBILE : ITEM_WIDTH;
return Math.floor(
Math.min(width - GRID_PADDING - TOOLBAR_WIDTH, MAX_GRID_WIDTH) / ITEM_WIDTH
Math.min(width - GRID_PADDING - TOOLBAR_WIDTH, MAX_GRID_WIDTH) / itemWidth
);
};

View File

@@ -34,4 +34,30 @@ export default () => {
if (normalizedColor.isValid()) setColor(normalizedColor.toHexString());
}
}, [color, setColor]);
useEffect(() => {
if (!weight && !size && !color) {
const persistedState = JSON.parse(
window.localStorage.getItem("__phosphor_settings__") || "null"
);
if (!!persistedState) {
const { weight, size, color } = persistedState;
if (weight) {
if (weight.toUpperCase() in IconStyle) setWeight(weight as IconStyle);
}
if (size) {
const normalizedSize = parseInt(size);
if (typeof normalizedSize === "number" && isFinite(normalizedSize))
setSize(Math.min(Math.max(normalizedSize, 16), 96));
}
if (color) {
const normalizedColor = TinyColor(color);
if (normalizedColor.isValid())
setColor(normalizedColor.toHexString());
}
}
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, []);
};

View File

@@ -0,0 +1,18 @@
import { useRecoilValue } from "recoil";
import useDebounce from "./useDebounce";
import { iconWeightAtom, iconSizeAtom, iconColorAtom } from "../state/atoms";
export default function usePersistSettings() {
const weight = useRecoilValue(iconWeightAtom);
const size = useRecoilValue(iconSizeAtom);
const color = useRecoilValue(iconColorAtom);
useDebounce(
() => {
const serializedState = JSON.stringify({ weight, size, color });
window.localStorage.setItem("__phosphor_settings__", serializedState);
},
2000,
[weight, size, color]
);
}

37
src/hooks/useThrottle.ts Normal file
View File

@@ -0,0 +1,37 @@
import { useEffect, useRef, useState } from "react";
import useUnmount from "./useUnmount";
const useThrottle = <T>(value: T, ms: number = 200) => {
const [state, setState] = useState<T>(value);
const timeout = useRef<ReturnType<typeof setTimeout>>();
const nextValue = useRef(null) as any;
const hasNextValue = useRef(0) as any;
useEffect(() => {
if (!timeout.current) {
setState(value);
const timeoutCallback = () => {
if (hasNextValue.current) {
hasNextValue.current = false;
setState(nextValue.current);
timeout.current = setTimeout(timeoutCallback, ms);
} else {
timeout.current = undefined;
}
};
timeout.current = setTimeout(timeoutCallback, ms);
} else {
nextValue.current = value;
hasNextValue.current = true;
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [value]);
useUnmount(() => {
timeout.current && clearTimeout(timeout.current);
});
return state;
};
export default useThrottle;

44
src/hooks/useTimeoutFn.ts Normal file
View File

@@ -0,0 +1,44 @@
import { useCallback, useEffect, useRef } from "react";
export type UseTimeoutFnReturn = [() => boolean | null, () => void, () => void];
export default function useTimeoutFn(
fn: Function,
ms: number = 0
): UseTimeoutFnReturn {
const ready = useRef<boolean | null>(false);
const timeout = useRef<ReturnType<typeof setTimeout>>();
const callback = useRef(fn);
const isReady = useCallback(() => ready.current, []);
const set = useCallback(() => {
ready.current = false;
timeout.current && clearTimeout(timeout.current);
timeout.current = setTimeout(() => {
ready.current = true;
callback.current();
}, ms);
}, [ms]);
const clear = useCallback(() => {
ready.current = null;
timeout.current && clearTimeout(timeout.current);
}, []);
// update ref when function changes
useEffect(() => {
callback.current = fn;
}, [fn]);
// set on mount, clear on unmount
useEffect(() => {
set();
return clear;
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [ms]);
return [isReady, clear, set];
}

12
src/hooks/useUnmount.ts Normal file
View File

@@ -0,0 +1,12 @@
import { useRef, useEffect } from "react";
const useUnmount = (fn: () => any): void => {
const fnRef = useRef(fn);
// update the ref each render so if it change the newest callback will be invoked
fnRef.current = fn;
useEffect(() => () => fnRef.current(), []);
};
export default useUnmount;

File diff suppressed because it is too large Load Diff

View File

@@ -13,7 +13,7 @@ export const iconWeightAtom = atom<IconStyle>({
export const iconSizeAtom = atom<number>({
key: "iconSizeAtom",
default: 48,
default: 32,
});
export const iconColorAtom = atom<string>({

View File

@@ -2,7 +2,12 @@ import { selector, selectorFamily } from "recoil";
import TinyColor from "tinycolor2";
import Fuse from "fuse.js";
import { searchQueryAtom, iconColorAtom } from "./atoms";
import {
searchQueryAtom,
iconWeightAtom,
iconSizeAtom,
iconColorAtom,
} from "./atoms";
import { IconEntry, IconCategory } from "../lib";
import { icons } from "../lib/icons";
@@ -52,17 +57,29 @@ export const singleCategoryQueryResultsSelector = selectorFamily<
IconCategory
>({
key: "singleCategoryQueryResultsSelector",
get: (category: IconCategory) => ({ get }) => {
const filteredResults = get(filteredQueryResultsSelector);
return new Promise((resolve) =>
resolve(
filteredResults.filter((icon) => icon.categories.includes(category))
)
);
},
get:
(category: IconCategory) =>
({ get }) => {
const filteredResults = get(filteredQueryResultsSelector);
return new Promise((resolve) =>
resolve(
filteredResults.filter((icon) => icon.categories.includes(category))
)
);
},
});
export const isDarkThemeSelector = selector<boolean>({
key: "isDarkThemeSelector",
get: ({ get }) => TinyColor(get(iconColorAtom)).isLight(),
});
export const resetSettingsSelector = selector<null>({
key: "resetSettings",
get: () => null,
set: ({ reset }) => {
reset(iconWeightAtom);
reset(iconSizeAtom);
reset(iconColorAtom);
},
});