35 Commits

Author SHA1 Message Date
Tobias Fried
675c4fc364 Merge pull request #221 from phosphor-icons/revamp
Revamp
2023-03-08 01:08:51 -07:00
rektdeckard
85af26fd1c feat(app): various updates for v2.0.0 2023-03-08 01:08:32 -07:00
rektdeckard
d379cea5bc feat(app): banner, style tweaks 2023-03-08 01:07:01 -07:00
rektdeckard
baeec27267 chore(app): clean up links, metadata 2023-03-08 01:07:01 -07:00
rektdeckard
345acafb45 feat(app): major refactorings and details footer updates 2023-03-08 01:07:01 -07:00
rektdeckard
eba876b3ea feat(app): new details footer appearance 2023-03-08 01:07:01 -07:00
rektdeckard
3756374140 feat(analytics): migrate to GA4 2023-03-08 01:07:01 -07:00
rektdeckard
5e7f85ffdc feat(app): tabbed sticky details panel 2023-03-08 01:07:01 -07:00
rektdeckard
1b8d6c48fc chore(build): move to react 18 + vite 2023-03-08 01:07:01 -07:00
Tobias Fried
ced8732edb Merge pull request #209 from lucagoslar/master
chore(docs): list phosphor-css in community projects
2023-01-15 20:13:08 -05:00
Luca Goslar
c0d84d9aec docs(Community-Projects): list phosphor-css 2023-01-11 18:52:09 +01:00
rektdeckard
2a030b08cd refactor(app): switch to @phosphor-icons/core and format files 2023-01-08 02:10:49 -07:00
Tobias Fried
b708d823a7 Merge pull request #156 from PH7-Jack/patch-1
add wireui phosphor icons port
2022-07-14 20:42:40 -04:00
Pedro Oliveira
59285244ac add wireui phosphor icons port 2022-07-14 01:34:51 -03:00
rektdeckard
511c191a02 chore(app)L remove figma banner 2022-05-10 22:55:05 -06:00
rektdeckard
a9ce9faac8 chore(app): add vote analytics 2022-05-08 14:24:20 -06:00
rektdeckard
bda9942fff feat(app): add figma banner 2022-05-08 14:14:30 -06:00
rektdeckard
c3cd3c1d80 refactor(ui): clean up snippets code 2021-12-29 01:06:25 -05:00
rektdeckard
4137a8b5c1 feat(app): Add flutter snippets and esc-to-close on panel 2021-12-24 16:21:42 -05:00
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
79 changed files with 1683 additions and 10035 deletions

1
.gitignore vendored
View File

@@ -10,6 +10,7 @@
# production
/build
/dist
# misc
.DS_Store

View File

@@ -4,7 +4,7 @@
Phosphor is a flexible icon family for interfaces, diagrams, presentations — whatever, really.
- 772 icons and counting
- 1,248 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
@@ -13,9 +13,9 @@ More ways to use at [phosphoricons.com](https://phosphoricons.com).
## For developers
Phosphor is available as a [one-liner](https://github.com/phosphor-icons/phosphor-icons) script, [React package](https://github.com/phosphor-icons/phosphor-react), and [Vue package](https://github.com/phosphor-icons/phosphor-vue), all of which can be sourced from NPM or from a CDN.
Phosphor is available for [web](https://github.com/phosphor-icons/web), [React](https://github.com/phosphor-icons/react), [Vue](https://github.com/phosphor-icons/vue), [Flutter](https://github.com/phosphor-icons/flutter), [Elm](https://github.com/phosphor-icons/phosphor-elm), and other frameworks and platforms.
### HTML/CSS
### Vanilla Web
- **Simple to use** 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 add the script to the document `<head>`, and drop in icons with an `<i/>` tag and the appropriate class:
@@ -23,26 +23,26 @@ Phosphor is available as a [one-liner](https://github.com/phosphor-icons/phospho
<!DOCTYPE html>
<html>
<head>
<script src="https://unpkg.com/phosphor-icons"></script>
<script src="https://unpkg.com/@phosphor-icons/web"></script>
</head>
<body>
<i class="ph-smiley"></i>
<i class="ph-heart-fill" style="color: hotpink"></i>
<i class="ph-cube-thin"></i>
<i class="ph-fill ph-heart" style="color: hotpink"></i>
<i class="ph-thin ph-cube"></i>
</body>
</html>
```
Check out the full documentation on the [phosphor-icons](https://github.com/phosphor-icons/phosphor-icons) repo page.
Check out the full documentation on the [@phosphor-icons/web](https://github.com/phosphor-icons/web) repo page.
### React
- **Powerful** Phosphor's intuitive but powerful API can style the `color`, `size`, and `weight` of an icon with a few keystrokes, provide default styles to all icons via the Context API, or directly manipulate the SVG at runtime through render props to do some amazing things! Check out the full documentation on the [phosphor-react](https://github.com/phosphor-icons/phosphor-react) repo page.
- **Powerful** Phosphor's intuitive but powerful API can style the `color`, `size`, and `weight` of an icon with a few keystrokes, provide default styles to all icons via the Context API, or directly manipulate the SVG at runtime through render props to do some amazing things! Check out the full documentation on the [@phosphor-icons/react](https://github.com/phosphor-icons/react) repo page.
```jsx
import React from "react";
import ReactDOM from "react-dom";
import { Smiley, Heart, Horse } from "phosphor-react";
import { Smiley, Heart, Horse } from "@phosphor-icons/react";
const App = () => {
return (
@@ -62,7 +62,7 @@ ReactDOM.render(<App />, document.getElementById("root"));
### Vue
- **Parity** As with React, you can manipulate the `color`, `size`, and `weight` of an icon with a few keystrokes, or provide default styles to all icons via the `provide/inject` API. It is fully tree-shakable and ready to use right away. Check out the full documentation on the [phosphor-vue](https://github.com/phosphor-icons/phosphor-vue) repo page.
- **Parity** As with React, you can manipulate the `color`, `size`, and `weight` of an icon with a few keystrokes, or provide default styles to all icons via the `provide/inject` API. It is fully tree-shakable and ready to use right away. Check out the full documentation on the [@phosphor-icons/vue](https://github.com/phosphor-icons/vue) repo page.
```html
<template>
@@ -88,14 +88,28 @@ 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
- [phosphor-icons](https://github.com/phosphor-icons/phosphor-icons) ▲ Phosphor icons for Vanilla JS
- [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-icons/core](https://github.com/phosphor-icons/core) ▲ Phosphor icon assets and catalog
- [@phosphor-icons/react](https://github.com/phosphor-icons/react) ▲ Phosphor icon component library for React
- [@phosphor-icons/web](https://github.com/phosphor-icons/web) ▲ Phosphor icons for Vanilla JS
- [@phosphor-icons/vue](https://github.com/phosphor-icons/vue) ▲ Phosphor icon component library for Vue
- [@phosphor-icons/elm](https://github.com/phosphor-icons/phosphor-elm) ▲ Phosphor icons for Elm
- [@phosphor-icons/flutter](https://github.com/phosphor-icons/flutter) ▲ Phosphor IconData library for Flutter
- [@phosphor-icons/webcomponents](https://github.com/phosphor-icons/webcomponents) ▲ Phosphor icons as Web Components
- [@phosphor-icons/figma](https://github.com/phosphor-icons/figma) ▲ Phosphor icons Figma plugin
- [@phosphor-icons/sketch](https://github.com/phosphor-icons/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
- [wireui/phosphoricons](https://github.com/wireui/phosphoricons) ▲ Phosphor icons for Laravel
- [phosphor-css](https://github.com/lucagoslar/phosphor-css) ▲ CSS wrapper for Phosphor SVG icons
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

View File

@@ -3,7 +3,7 @@
<head>
<meta charset="utf-8" />
<title>Phosphor Icons</title>
<link rel="icon" href="%PUBLIC_URL%/favicon.ico" />
<link rel="icon" href="/favicon.ico" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<meta name="theme-color" content="#35313D" />
<meta
@@ -66,20 +66,20 @@
<meta name="twitter:site" content="@_phosphoricons" />
<meta name="twitter:creator" content="@friedtm" />
<link rel="apple-touch-icon" href="%PUBLIC_URL%/favicon-192.png" />
<link rel="apple-touch-icon" href="/favicon-192.png" />
<link
rel="icon"
type="image/png"
sizes="32x32"
href="%PUBLIC_URL%/favicon-32x32.png"
href="/favicon-32x32.png"
/>
<link
rel="icon"
type="image/png"
sizes="16x16"
href="%PUBLIC_URL%/favicon-16x16.png"
href="/favicon-16x16.png"
/>
<link rel="manifest" href="%PUBLIC_URL%/manifest.json" />
<link rel="manifest" href="/manifest.json" />
<link
href="https://fonts.googleapis.com/css2?family=Manrope:wght@400;600&display=swap"
rel="stylesheet"
@@ -112,5 +112,6 @@
</div>
</noscript>
<div id="root"></div>
<script type="module" src="/src/index.tsx"></script>
</body>
</html>

View File

@@ -1,6 +1,6 @@
{
"name": "phosphor-home",
"version": "1.2.1",
"version": "2.0.0",
"license": "MIT",
"homepage": "https://phosphoricons.com",
"author": {
@@ -18,33 +18,41 @@
"UI",
"UX"
],
"repository": "github:phosphor-icons/phosphor-home",
"repository": "github:phosphor-icons/homepage",
"private": true,
"scripts": {
"dev": "vite",
"build": "tsc && vite build",
"preview": "vite preview",
"format": "prettier --write \"./src/**/*.{js,jsx,ts,tsx,json,vue}\""
},
"dependencies": {
"@phosphor-icons/core": "^2.0.2",
"@phosphor-icons/react": "^2.0.4",
"file-saver": "^2.0.2",
"framer-motion": "^3.10.0",
"framer-motion": "^9.0.1",
"fuse.js": "^6.4.1",
"phosphor-react": "^1.2.1",
"react": "^17.0.1",
"react-dom": "^17.0.1",
"prop-types": "^15.8.1",
"react": "^18.2.0",
"react-dom": "^18.2.0",
"react-dropdown-select": "^4.4.2",
"react-ga": "^3.1.2",
"react-ga4": "^2.0.0",
"react-hotkeys-hook": "^3.2.1",
"react-scripts": "3.4.1",
"react-use": "^15.3.2",
"recoil": "^0.1.3",
"svg2png-converter": "^1.0.0",
"react-use": "^17.4.0",
"recoil": "^0.7.6",
"svg2png-converter": "^1.0.2",
"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": {
"@types/file-saver": "^2.0.5",
"@types/node": "^18.11.18",
"@types/react": "^18.0.27",
"@types/react-dom": "^18.0.10",
"@types/tinycolor2": "^1.4.3",
"@vitejs/plugin-react": "^3.1.0",
"typescript": "^4.9.5",
"vite": "^4.1.1",
"vite-plugin-svgr": "^2.4.0"
},
"eslintConfig": {
"extends": "react-app"
@@ -60,19 +68,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

@@ -1,8 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<svg width="58px" height="58px" viewBox="0 0 58 58" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
<title>u-arrow-up-left</title>
<g id="u-arrow-up-left" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd">
<polygon id="bounding-box" transform="translate(29.200000, 29.200000) scale(1, -1) rotate(90.000000) translate(-29.200000, -29.200000) " points="0.4 58 58 58 58 0.4 0.4 0.4"></polygon>
<path d="M38.2,9.4 C39.1941125,9.4 40,10.2058875 40,11.2 C40,12.1444069 39.2726866,12.9189407 38.347628,12.9940331 L38.2,13 L18.4,13 C12.4353125,13 7.6,17.8353125 7.6,23.8 C7.6,29.6684828 12.2805926,34.4437284 18.112283,34.5962419 L18.4,34.6 L44.654,34.6 L36.9272078,26.8727922 C36.2633165,26.2089009 36.2264336,25.1554365 36.8165592,24.4482593 L36.9272078,24.3272078 C37.5910991,23.6633165 38.6445635,23.6264336 39.3517407,24.2165592 L39.4727922,24.3272078 L50.2727922,35.1272078 C50.3086835,35.1630991 50.3430661,35.200499 50.3758368,35.2393043 L50.2727922,35.1272078 C50.322523,35.1769386 50.3687356,35.2288554 50.4114298,35.2826489 C50.4303122,35.3066321 50.4485184,35.3308286 50.4661181,35.3554879 C50.4786457,35.3727743 50.4909825,35.3907223 50.5029573,35.4088392 C50.5204983,35.4356771 50.5373395,35.4626973 50.5534721,35.4901836 C50.5604155,35.501718 50.5673022,35.5137723 50.5740382,35.5258878 C50.5905898,35.5559164 50.606343,35.5862258 50.6212445,35.6170243 C50.6310894,35.6372379 50.6403272,35.6573209 50.6491756,35.6775332 C50.6615763,35.705848 50.6734867,35.7350368 50.6846414,35.7645963 C50.6896953,35.7781611 50.6944267,35.7911991 50.6990025,35.8042794 C50.7106655,35.837281 50.721476,35.8712903 50.7312871,35.9057207 C50.7358977,35.9224356 50.7403085,35.938831 50.7444838,35.9552738 C50.7807852,36.0974825 50.8,36.2464973 50.8,36.4 C50.8,36.6494086 50.7492745,36.8869696 50.6575775,37.1029292 C50.6430903,37.1371689 50.6273615,37.1712925 50.6105085,37.2049973 C50.5965597,37.232636 50.5820362,37.2598508 50.5668455,37.2866377 C50.566621,37.2873207 50.5664341,37.2876503 50.566247,37.2879798 C50.5435288,37.327754 50.5186404,37.3678619 50.492262,37.4068799 C50.4795991,37.4258795 50.4663526,37.4446817 50.4527038,37.463281 C50.4342916,37.488172 50.4153249,37.512692 50.3957513,37.5366974 C50.3915469,37.5419745 50.3875087,37.5468658 50.3834408,37.5517407 L50.3644975,37.5739923 C50.3400259,37.6024091 50.3146818,37.630053 50.2885067,37.6568825 L50.2727922,37.6727922 L39.4727922,48.4727922 C38.7698485,49.1757359 37.6301515,49.1757359 36.9272078,48.4727922 C36.2633165,47.8089009 36.2264336,46.7554365 36.8165592,46.0482593 L36.9272078,45.9272078 L44.654,38.2 L18.4,38.2 C10.4470875,38.2 4,31.7529125 4,23.8 C4,15.951731 10.2785434,9.5699209 18.0868626,9.4033373 L18.4,9.4 L38.2,9.4 Z" fill="#000000" fill-rule="nonzero" transform="translate(27.400000, 29.200000) scale(1, -1) rotate(90.000000) translate(-27.400000, -29.200000) "></path>
</g>
</svg>

Before

Width:  |  Height:  |  Size: 2.9 KiB

View File

@@ -1,3 +1,21 @@
:root {
--red: #ff6e60;
--orange: #ff8e51;
--yellow: #ffd171;
--pale: #ffe8dc;
--peach: #ffd5c0;
--darkgreen: #245633;
--blue: #397fff;
--purple: #925bff;
--eggplant: #35313d;
--neutral: #86838b;
--translucent: rgba(163, 159, 171, 0.1);
--scrim: rgba(255, 255, 255, 0.05);
--sheer: rgba(194, 186, 196, 0.25);
--soft: rgba(194, 186, 196, 0.7);
--shadow: rgba(0, 0, 0, 0.15);
}
body {
margin: 0px;
font-variant-ligatures: common-ligatures;
@@ -24,16 +42,14 @@ img {
pre,
code {
font-family: "IBM Plex Mono", "Courier New", monospace;
font-size: 14px;
font-size: 12px;
}
pre {
box-sizing: border-box;
padding: 20px 16px 20px 24px;
margin: 12px 0px;
background-color: white;
margin: 0;
border-radius: 6px;
border: 1px solid #e1d4d7;
font-size: 12x;
white-space: pre-wrap;
}
@@ -47,6 +63,7 @@ button {
display: flex;
align-items: center;
justify-content: flex-start;
cursor: pointer;
}
button.main-button {
@@ -77,14 +94,6 @@ button.main-button:active {
box-shadow: 0 0 0 0 black;
}
button.main-button:focus {
outline: none;
}
/* button.main-button:not(:last-child) {
margin: 0 24px 24px 0;
} */
button.main-button svg {
margin-right: 12px;
}
@@ -109,8 +118,37 @@ 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: var(--red);
}
.badge.updated {
color: var(--blue);
}
.badge {
font-size: 24px;
line-height: 0.5em;
}
.card {
border-radius: 8px;
border: 2px solid var(--translucent);
}
.primary {
color: var(--foreground);
background-color: var(--background);
}
.secondary {
color: var(--foreground-card);
background-color: var(--background-card);
}

View File

@@ -1,9 +0,0 @@
import React from 'react';
import { render } from '@testing-library/react';
import App from './App';
test('renders learn react link', () => {
const { getByText } = render(<App />);
const linkElement = getByText(/learn react/i);
expect(linkElement).toBeInTheDocument();
});

View File

@@ -1,22 +1,43 @@
import React, { Suspense } from "react";
import { Fragment, Suspense, useMemo } from "react";
import { useRecoilValue } from "recoil";
import "./App.css";
import Header from "../Header/Header";
import Toolbar from "../Toolbar/Toolbar";
import IconGrid from "../IconGrid/IconGrid";
import Footer from "../Footer/Footer";
import ErrorBoundary from "../ErrorBoundary/ErrorBoundary";
import Notice from "../Notice/Notice";
import useIconParameters from "../../hooks/useIconParameters";
import Header from "@/components/Header";
import Toolbar from "@/components/Toolbar";
import IconGrid from "@/components/IconGrid";
import Footer from "@/components/Footer";
import ErrorBoundary from "@/components/ErrorBoundary";
import Notice from "@/components/Notice";
import {
useIconParameters,
usePersistSettings,
useCSSVariables,
} from "@/hooks";
import { isDarkThemeSelector } from "@/state";
const errorFallback = <Notice message="Search error" />;
const waitingFallback = <Notice type="none" message="" />;
const App: React.FC<any> = () => {
useIconParameters();
usePersistSettings();
const isDark = useRecoilValue(isDarkThemeSelector);
const properties = useMemo(
() => ({
"--foreground": isDark ? "white" : "black",
"--foreground-card": isDark ? "white" : "#35313D",
"--background": isDark ? "#35313D" : "white",
"--background-card": isDark ? "#413c48" : "#f6f5f6",
}),
[isDark]
);
useCSSVariables(properties);
return (
<React.StrictMode>
<Fragment>
<Header />
<main>
<Toolbar />
@@ -27,7 +48,7 @@ const App: React.FC<any> = () => {
</ErrorBoundary>
</main>
<Footer />
</React.StrictMode>
</Fragment>
);
};

View File

@@ -0,0 +1 @@
export { default } from "./App";

View File

@@ -0,0 +1,55 @@
.banner {
position: fixed;
top: 0;
left: 0;
right: 0;
border-radius: 0;
display: flex;
padding: 12px;
color: white;
margin: auto;
background-color: var(--eggplant);
z-index: 1;
}
.banner .main-button {
height: unset;
min-height: 64px;
margin: 8px 0 0;
}
.banner a {
color: white;
}
.banner-content {
display: flex;
align-items: center;
justify-content: space-between;
gap: 20px;
flex: 1;
max-width: 1120px;
margin: auto;
font-family: "IBM Plex Mono";
}
.banner-button {
color: inherit;
background: var(--eggplant);
height: unset !important;
padding: 0 !important;
margin: 0 !important;
border-radius: 48px !important;
cursor: pointer;
}
.banner-button:active {
opacity: 0.7;
}
.message {
display: grid;
grid-template-columns: 32px 1fr;
align-items: center;
gap: 20px;
}

View File

@@ -0,0 +1,81 @@
import { ReactNode, Dispatch, SetStateAction } from "react";
import { motion, AnimatePresence, Variants } from "framer-motion";
import { XCircle } from "@phosphor-icons/react";
import ReactGA from "react-ga4";
import { useLocalStorage } from "@/hooks";
import "./Banner.css";
type BannerState = {
seen: Record<string, boolean>;
};
type BannerProps = {
id: string;
children?: ReactNode;
onClose?: (dispatch: Dispatch<SetStateAction<BannerState>>) => void;
};
const variants: Variants = {
initial: { y: -120 },
animate: { y: 0 },
exit: { y: -120 },
};
const BANNER_STATE_KEY = "banner_state";
const Banner = ({ id, children, onClose }: BannerProps) => {
const [
{
seen: { [id]: seen },
},
setBannerState,
] = useLocalStorage<BannerState>(BANNER_STATE_KEY, {
seen: { [id]: false },
});
const handleClose = () => {
ReactGA.event({
category: "Banner",
action: "Dismiss",
label: id,
});
onClose
? onClose(setBannerState)
: setBannerState((state) => ({
...state,
seen: { ...state.seen, [id]: true },
}));
};
return (
<AnimatePresence initial={true}>
{!seen && (
<motion.aside
className="card banner"
initial="initial"
animate="animate"
exit="exit"
variants={variants}
>
<div className="banner-content">
{children}
<button
tabIndex={0}
className="banner-button"
onClick={handleClose}
onKeyDown={(e) => {
e.key === "Enter" && handleClose();
}}
>
<XCircle color="currentColor" size={28} weight="regular" />
</button>
</div>
</motion.aside>
)}
</AnimatePresence>
);
};
export default Banner;

View File

@@ -0,0 +1 @@
export { default } from "./Banner";

View File

@@ -1,14 +1,14 @@
import React, { useCallback } from "react";
import { useCallback } from "react";
import { useRecoilState, useRecoilValue } from "recoil";
import { iconColorAtom } from "../../state/atoms";
import { isDarkThemeSelector } from "../../state/selectors";
import useThrottled from "../../hooks/useThrottled";
import { useThrottled } from "@/hooks";
import { iconColorAtom, isDarkThemeSelector } from "@/state";
import "./ColorInput.css";
type ColorInputProps = {};
const ColorInput: React.FC<ColorInputProps> = () => {
const ColorInput = (_: ColorInputProps) => {
const [color, setColor] = useRecoilState(iconColorAtom);
const isDark = useRecoilValue(isDarkThemeSelector);

View File

@@ -0,0 +1 @@
export { default } from "./ColorInput";

View File

@@ -1,17 +1,21 @@
import React, { ErrorInfo } from "react";
import { Component, ErrorInfo, ReactNode } from "react";
interface ErrorBoundaryProps {
fallback?: JSX.Element | React.ReactNode;
fallback?: JSX.Element | ReactNode;
children?: JSX.Element | ReactNode;
}
interface ErrorBoundaryState {
errorMessage?: string
errorMessage?: string;
}
export default class ErrorBoundary extends React.Component<ErrorBoundaryProps, ErrorBoundaryState> {
export default class ErrorBoundary extends Component<
ErrorBoundaryProps,
ErrorBoundaryState
> {
constructor(props: any) {
super(props);
this.state = { errorMessage: "" }
this.state = { errorMessage: "" };
}
static getDerivedStateFromError(error: any) {
@@ -23,7 +27,7 @@ export default class ErrorBoundary extends React.Component<ErrorBoundaryProps, E
console.info(info);
}
render(): JSX.Element | React.ReactNode {
render(): JSX.Element | ReactNode {
if (this.state.errorMessage) {
return this.props.fallback ?? <p>{this.state.errorMessage}</p>;
}

View File

@@ -0,0 +1 @@
export { default } from "./ErrorBoundary";

View File

@@ -1,5 +1,5 @@
footer {
background-color: #925bff;
background-color: var(--purple);
}
#back-to-top-button {
@@ -8,6 +8,16 @@ footer {
margin: 0;
border-radius: 50%;
z-index: 2;
font-size: 56px;
}
#back-to-top-button:active {
transform: translate(4px, 4px) !important;
box-shadow: 0 0 0 0 black;
}
#back-to-top-button svg {
margin-right: unset;
}
.container {
@@ -65,11 +75,7 @@ footer .links {
width: 56px;
height: 56px;
padding: 0;
}
#back-to-top-button img {
width: 28px;
height: 28px;
font-size: 28px;
}
footer .links {
@@ -133,17 +139,11 @@ footer .links {
top: 276px;
}
/* #command {
position: absolute;
left: 532px;
top: 150px;
} */
.illustrations-footer {
display: initial;
position: absolute;
left: -240px;
top: 656px;
top: 632px;
height: 584px;
overflow: hidden;
}

View File

@@ -1,30 +1,51 @@
import React from "react";
import { Coffee, Heart } from "phosphor-react";
import { useRecoilValue } from "recoil";
import { motion, AnimatePresence, Variants } from "framer-motion";
import { Coffee, Heart, ArrowULeftUp } from "@phosphor-icons/react";
import uArrowUpLeft from "../../assets/u-arrow-up-left.svg";
import markerGreen from "../../assets/marker-green.svg";
import postIt from "../../assets/footer-mobile.svg";
import Links from "../Links/Links";
import Links from "@/components/Links/Links";
import { ReactComponent as MarkerGreen } from "@/assets/marker-green.svg";
import { ReactComponent as PostIt } from "@/assets/footer-mobile.svg";
import { useMediaQuery } from "@/hooks";
import { selectionEntryAtom } from "@/state";
import "./Footer.css";
type FooterProps = {};
const Footer: React.FC<FooterProps> = () => {
const variants: Variants = {
initial: { y: 188 },
animate: { y: 0 },
exit: { y: 188 },
};
const Footer = (_: FooterProps) => {
const isMobile = useMediaQuery("(max-width: 719px)");
const isViewing = !!useRecoilValue(selectionEntryAtom);
return (
<footer>
<div className="container">
<button
<AnimatePresence initial={false}>
{(!isMobile || !isViewing) && (
<motion.button
id="back-to-top-button"
aria-label="back-to-top button"
className="main-button"
variants={variants}
initial="initial"
animate="animate"
exit="exit"
transition={{ duration: 0.1 }}
onClick={() => {
document
.getElementById("root")
?.scrollIntoView({ behavior: "smooth", block: "start" });
}}
>
<img src={uArrowUpLeft} alt="" />
</button>
<ArrowULeftUp size="1em" />
</motion.button>
)}
</AnimatePresence>
<div className="outro">
<Links />
<p>
@@ -34,8 +55,8 @@ const Footer: React.FC<FooterProps> = () => {
a little quirky, too.
</p>
<p>
We're thankful for the tools we've benefited from and
this is our contribution towards a collaborative community.
We're thankful for the tools we've benefited from and this is our
contribution towards a collaborative community.
</p>
<p>
Phosphor is free and open-source, licensed under{" "}
@@ -95,24 +116,17 @@ const Footer: React.FC<FooterProps> = () => {
<a className="main-link" href="mailto:hello@phosphoricons.com">
hello@phosphoricons.com
</a>
. Check out our sister project:{" "}
<a
className="main-link"
href="https://play.google.com/store/apps/details?id=com.tobiasfried.phosphor"
>
Phosphor for Android
</a>
. Type set in{" "}
<a className="main-link" href="https://manropefont.com/">
Manrope
</a>{" "}
by Mikhail Sharanda.
</p>
<img id="marker-green" src={markerGreen} alt="" />
<MarkerGreen id="marker-green" />
</div>
</div>
<div className="illustrations-footer">
<img id="post-it" src={postIt} width="878" height="667" alt="" />
<PostIt id="post-it" width="878" height="667" />
</div>
</div>
</footer>

View File

@@ -0,0 +1 @@
export { default } from "./Footer";

View File

@@ -1,7 +1,7 @@
header {
width: 100%;
background-color: #ffd171;
overflow: hidden;
position: relative;
background-color: var(--yellow);
}
.header-contents {

View File

@@ -1,29 +1,35 @@
import React from "react";
import { ArrowCircleUpRight, ArrowCircleDown } from "phosphor-react";
import {
ArrowCircleUpRight,
ArrowCircleDown,
Broadcast,
} from "@phosphor-icons/react";
import markerPurple from "../../assets/marker-purple.svg";
import paperclips from "../../assets/paperclips-header-mobile.svg";
import paperclipsThree from "../../assets/paperclips-header.svg";
import tablet from "../../assets/tablet.svg";
import tabletSpec from "../../assets/tablet-spec.svg";
import billiardBall from "../../assets/billiard-ball.svg";
import billiardBallSpec from "../../assets/billiard-ball-spec.svg";
import warning from "../../assets/warning.svg";
import warningSpec from "../../assets/warning-spec.svg";
import cuttingMat from "../../assets/cutting-mat.svg";
import cuttingMatSpec from "../../assets/cutting-mat-spec.svg";
import receipt from "../../assets/receipt.svg";
import receiptSpec from "../../assets/receipt-spec.svg";
import calculator from "../../assets/calculator.svg";
import calculatorSpec from "../../assets/calculator-spec.svg";
import Links from "../Links/Links";
import Banner from "@/components/Banner";
import { ReactComponent as MarkerPurple } from "@/assets/marker-purple.svg";
import { ReactComponent as PaperClips } from "@/assets/paperclips-header-mobile.svg";
import { ReactComponent as PaperClipsThree } from "@/assets/paperclips-header.svg";
import { ReactComponent as Tablet } from "@/assets/tablet.svg";
import { ReactComponent as TabletSpec } from "@/assets/tablet-spec.svg";
import { ReactComponent as BilliardBall } from "@/assets/billiard-ball.svg";
import { ReactComponent as BilliardBallSpec } from "@/assets/billiard-ball-spec.svg";
import { ReactComponent as Warning } from "@/assets/warning.svg";
import { ReactComponent as WarningSpec } from "@/assets/warning-spec.svg";
import { ReactComponent as CuttingMat } from "@/assets/cutting-mat.svg";
import { ReactComponent as CuttingMatSpec } from "@/assets/cutting-mat-spec.svg";
import { ReactComponent as Receipt } from "@/assets/receipt.svg";
import { ReactComponent as ReceiptSpec } from "@/assets/receipt-spec.svg";
import { ReactComponent as Calculator } from "@/assets/calculator.svg";
import { ReactComponent as CalculatorSpec } from "@/assets/calculator-spec.svg";
import Links from "@/components/Links";
import "./Header.css";
type HeaderProps = {};
const handleGetStarted = () =>
window.open(
"https://github.com/phosphor-icons/phosphor-home#phosphor-icons",
"https://github.com/phosphor-icons/homepage#phosphor-icons",
"_blank",
"noopener noreferrer"
);
@@ -33,25 +39,34 @@ const handleScrollToIcons = () =>
.getElementById("toolbar")
?.scrollIntoView({ behavior: "smooth", block: "start" });
const Header: React.FC<HeaderProps> = () => {
const Header = (_: HeaderProps) => {
return (
<header>
<Banner id="2.0">
<div className="message">
<Broadcast size={32} weight="fill" />
<small>
Phosphor 2.0 is out, with some big updates and some small API
changes. Check our{" "}
<a href="https://github.com/phosphor-icons/homepage#readme">
documentation
</a>{" "}
to see what's new!
</small>
</div>
</Banner>
<div className="header-contents">
<div className="illustrations-top">
<img src={markerPurple} id="marker-purple" alt="" />
<img src={paperclips} id="paperclips" alt="" />
<img src={paperclipsThree} id="paperclips-three" alt="" />
<img className="tablet" src={tabletSpec} alt="" />
<img className="tablet inspectable xray" src={tablet} alt="" />
<img className="billiard-ball" src={billiardBallSpec} alt="" />
<img
className="billiard-ball inspectable xray"
src={billiardBall}
alt=""
/>
<MarkerPurple id="marker-purple" />
<PaperClips id="paperclips" />
<PaperClipsThree id="paperclips-three" />
<TabletSpec className="tablet" />
<Tablet className="tablet inspectable xray" />
<BilliardBallSpec className="billiard-ball" />
<BilliardBall className="billiard-ball inspectable xray" />
<img className="warning" src={warningSpec} alt="" />
<img className="warning inspectable xray" src={warning} alt="" />
<WarningSpec className="warning" />
<Warning className="warning inspectable xray" />
</div>
<div className="intro">
<h2>
@@ -73,20 +88,12 @@ const Header: React.FC<HeaderProps> = () => {
<Links />
</div>
<div className="illustrations-bottom">
<img className="cutting-mat" src={cuttingMatSpec} alt="" />
<img
className="cutting-mat inspectable xray"
src={cuttingMat}
alt=""
/>
<img className="receipt" src={receiptSpec} alt="" />
<img className="receipt inspectable xray" src={receipt} alt="" />
<img className="calculator" src={calculatorSpec} alt="" />
<img
className="calculator inspectable xray"
src={calculator}
alt=""
/>
<CuttingMatSpec className="cutting-mat" />
<CuttingMat className="cutting-mat inspectable xray" />
<ReceiptSpec className="receipt" />
<Receipt className="receipt inspectable xray" />
<CalculatorSpec className="calculator" />
<Calculator className="calculator inspectable xray" />
</div>
</div>
</header>

View File

@@ -0,0 +1 @@
export { default } from "./Header";

View File

@@ -0,0 +1,304 @@
import React, { useRef, useEffect, CSSProperties, useMemo } from "react";
import { useRecoilValue, useRecoilState } from "recoil";
import { useHotkeys } from "react-hotkeys-hook";
import { motion, AnimatePresence, Variants } from "framer-motion";
import { Svg2Png } from "svg2png-converter";
import { saveAs } from "file-saver";
import {
Copy,
CheckCircle,
DownloadSimple,
XCircle,
} from "@phosphor-icons/react";
import ReactGA from "react-ga4";
import Tabs, { Tab } from "@/components/Tabs";
import { useMediaQuery, useTransientState, useSessionStorage } from "@/hooks";
import { SnippetType } from "@/lib";
import {
iconWeightAtom,
iconSizeAtom,
iconColorAtom,
selectionEntryAtom,
isDarkThemeSelector,
} from "@/state";
import { getCodeSnippets, supportsWeight } from "@/utils";
import TagCloud from "./TagCloud";
const variants: Record<string, Variants> = {
desktop: {
initial: { y: 188 },
animate: { y: 0 },
exit: { y: 188 },
},
mobile: {
initial: { y: "60vh" },
animate: { y: 0 },
exit: { y: "60vh" },
},
};
const RENDERED_SNIPPETS = [
SnippetType.REACT,
SnippetType.VUE,
SnippetType.HTML,
SnippetType.FLUTTER,
SnippetType.ELM,
];
const buttonColor = "#35313D";
const successColor = "#1FA647";
const disabledColor = "#B7B7B7";
function cloneWithSize(svg: SVGSVGElement, size: number): SVGSVGElement {
const sized = svg.cloneNode(true) as SVGSVGElement;
sized.setAttribute("width", `${size}`);
sized.setAttribute("height", `${size}`);
return sized;
}
const DetailFooter = () => {
const [entry, setSelectionEntry] = useRecoilState(selectionEntryAtom);
const weight = useRecoilValue(iconWeightAtom);
const size = useRecoilValue(iconSizeAtom);
const color = useRecoilValue(iconColorAtom);
const isDark = useRecoilValue(isDarkThemeSelector);
const [copied, setCopied] = useTransientState<SnippetType | "SVG" | false>(
false,
2000
);
const ref = useRef<SVGSVGElement>(null);
const [{ i }, setInitialTab] = useSessionStorage("tab", { i: 0 });
const isMobile = useMediaQuery("(max-width: 719px)");
const [snippets, tabs] = useMemo<
[Partial<Record<SnippetType, string>>, Tab[]]
>(() => {
if (!entry) return [{}, []];
const snippets = getCodeSnippets({
displayName: entry?.pascal_name!,
name: entry.name,
weight,
size,
color,
});
const snippetButtonStyle: CSSProperties =
weight === "duotone"
? { color: disabledColor, userSelect: "none" }
: { color: "currentcolor" };
const tabs = [
{
header: "Tags",
content: (
<TagCloud
name={entry.name}
tags={Array.from(
new Set<string>([
...entry.categories,
...entry.name.split("-"),
...entry.tags,
])
)}
/>
),
},
].concat(
RENDERED_SNIPPETS.map((type) => {
const isWeightSupported = supportsWeight({ type, weight });
return {
header: type,
content: (
<div className="snippet" key={type}>
<pre style={isWeightSupported ? undefined : snippetButtonStyle}>
<span>
{isWeightSupported
? snippets[type]
: "This weight is not yet supported"}
</span>
<button
title="Copy snippet"
onClick={(e) => handleCopySnippet(e, type)}
disabled={!isWeightSupported}
style={
isWeightSupported
? { color: "currentColor" }
: snippetButtonStyle
}
>
{copied === type ? (
<CheckCircle size={24} color={successColor} weight="fill" />
) : (
<Copy
size={24}
color={
isWeightSupported
? "currentColor"
: snippetButtonStyle.color
}
weight="fill"
/>
)}
</button>
</pre>
</div>
),
};
})
);
return [snippets, tabs];
}, [entry, weight, size, copied, isDark]);
useHotkeys("esc", () => setSelectionEntry(null));
useEffect(() => {
if (!entry) return;
ReactGA.event({
category: "Grid",
action: "Details",
label: entry.name,
});
}, [entry]);
const buttonBarStyle: CSSProperties = {
color: isDark ? "white" : buttonColor,
backgroundColor: "transparent",
};
const handleCopySnippet = (
event: React.MouseEvent<HTMLButtonElement, MouseEvent>,
type: SnippetType
) => {
event.currentTarget.blur();
if (!entry) return;
setCopied(type);
const data = snippets[type];
data && void navigator.clipboard?.writeText(data);
};
const handleCopySVG = (
event: React.MouseEvent<HTMLButtonElement, MouseEvent>
) => {
event.currentTarget.blur();
if (!entry) return;
if (!ref.current) return;
navigator.clipboard?.writeText(cloneWithSize(ref.current, size).outerHTML);
setCopied("SVG");
};
const handleDownloadSVG = (
event: React.MouseEvent<HTMLButtonElement, MouseEvent>
) => {
event.currentTarget.blur();
if (!entry) return;
if (!ref.current) return;
const blob = new Blob([cloneWithSize(ref.current, size).outerHTML]);
saveAs(
blob,
`${entry?.name}${weight === "regular" ? "" : `-${weight}`}.svg`
);
};
const handleDownloadPNG = async (
event: React.MouseEvent<HTMLButtonElement, MouseEvent>
) => {
event.currentTarget.blur();
if (!entry) return;
if (!ref.current) return;
Svg2Png.save(
cloneWithSize(ref.current, size),
`${entry?.name}${weight === "regular" ? "" : `-${weight}`}.png`
);
};
return (
<AnimatePresence initial={true}>
{!!entry && (
<motion.aside
initial="initial"
animate="animate"
exit="exit"
variants={isMobile ? variants.mobile : variants.desktop}
className="secondary detail-footer card"
transition={isMobile ? { duration: 0.25 } : { duration: 0.1 }}
>
<div className="detail-preview">
<figure>
<entry.Icon ref={ref} size={64}></entry.Icon>
<figcaption>
<p>{entry.name}</p>
<small className="versioning">
available in v{entry.published_in.toFixed(1)}.0+
</small>
</figcaption>
</figure>
<div className="detail-actions">
<button
className="action-button"
tabIndex={0}
style={buttonBarStyle}
onClick={handleDownloadPNG}
>
<DownloadSimple size={20} color="currentColor" weight="fill" />{" "}
PNG
</button>
<button
className="action-button"
tabIndex={0}
style={buttonBarStyle}
onClick={handleDownloadSVG}
>
<DownloadSimple size={20} color="currentColor" weight="fill" />{" "}
SVG
</button>
<button
className="action-button"
tabIndex={0}
style={buttonBarStyle}
onClick={handleCopySVG}
>
{copied === "SVG" ? (
<CheckCircle size={20} color={successColor} weight="fill" />
) : (
<Copy size={20} color="currentColor" weight="fill" />
)}
{copied === "SVG" ? "Copied!" : " SVG"}
</button>
</div>
</div>
<Tabs
tabs={tabs}
initialIndex={i}
onTabChange={(i) => setInitialTab({ i })}
/>
<button
tabIndex={0}
className="close-button"
onClick={() => setSelectionEntry(null)}
onKeyDown={(e) => {
e.key === "Enter" && setSelectionEntry(null);
}}
>
<XCircle color="currentColor" size={28} weight="fill" />
</button>
</motion.aside>
)}
</AnimatePresence>
);
};
export default DetailFooter;

View File

@@ -1,263 +0,0 @@
import React, { useRef, useEffect } from "react";
import { useRecoilValue, useSetRecoilState } from "recoil";
import { motion } from "framer-motion";
import { Svg2Png } from "svg2png-converter";
import { saveAs } from "file-saver";
import { Copy, X, CheckCircle, Download } from "phosphor-react";
import ReactGA from "react-ga";
import {
iconWeightAtom,
iconSizeAtom,
iconColorAtom,
iconPreviewOpenAtom,
} from "../../state/atoms";
import useTransientState from "../../hooks/useTransientState";
import TagCloud from "./TagCloud";
import { IconEntry } from "../../lib";
const panelVariants = {
open: {
opacity: 1,
height: "100%",
marginTop: "4px",
marginBottom: "4px",
transition: { type: "tween", duration: 0.1 },
},
collapsed: {
opacity: 0,
height: "0px",
marginTop: "0px",
marginBottom: "0px",
transition: { type: "tween", duration: 0.1 },
},
};
const contentVariants = {
open: { opacity: 1, transition: { duration: 0.2, delay: 0.1 } },
collapsed: { opacity: 0, transition: { duration: 0 } },
};
const buttonColor = "#35313D";
const successColor = "#1FA647";
const disabledColor = "#B7B7B7";
interface InfoPanelProps {
index: number;
spans: number;
isDark: boolean;
entry: IconEntry;
}
const DetailsPanel: React.FC<InfoPanelProps> = (props) => {
const { index, spans, isDark, entry } = props;
const { name, Icon, categories, tags } = entry;
const weight = useRecoilValue(iconWeightAtom);
const size = useRecoilValue(iconSizeAtom);
const color = useRecoilValue(iconColorAtom);
const setOpen = useSetRecoilState(iconPreviewOpenAtom);
const [copied, setCopied] = useTransientState<string | false>(false, 2000);
const ref = useRef<SVGSVGElement>(null);
useEffect(
() => ReactGA.event({ category: "Grid", action: "Details", label: name }),
[name]
);
const buttonBarStyle: React.CSSProperties = {
color: isDark ? "white" : buttonColor,
};
const snippetButtonStyle: React.CSSProperties =
weight === "duotone"
? { color: disabledColor, userSelect: "none" }
: { color: buttonColor };
const snippets = {
html:
weight === "duotone"
? "This weight is not yet supported"
: `<i class="ph-${name}${
weight === "regular" ? "" : `-${weight}`
}"></i>`,
react: `<${Icon.displayName} size={${size}} ${
color !== "#000000" ? `color="${color}" ` : ""
}${weight === "regular" ? "" : `weight="${weight}" `}/>`,
vue: `<ph${Icon.displayName!!.replace(
/([a-z0-9]|(?=[A-Z]))([A-Z])/g,
"$1-$2"
).toLowerCase()} :size="${size}" ${
color !== "#000000" ? `color="${color}" ` : ""
}${weight === "regular" ? "" : `weight="${weight}" `}/>`,
};
const handleCopySnippet = (
event: React.MouseEvent<HTMLButtonElement, MouseEvent>,
type: "html" | "react" | "vue"
) => {
event.currentTarget.blur();
setCopied(type);
const data = snippets[type];
data && void navigator.clipboard?.writeText(data);
};
const handleCopySVG = (
event: React.MouseEvent<HTMLButtonElement, MouseEvent>
) => {
event.currentTarget.blur();
setCopied("svg");
ref.current && void navigator.clipboard?.writeText(ref.current.outerHTML);
};
const handleDownloadSVG = (
event: React.MouseEvent<HTMLButtonElement, MouseEvent>
) => {
event.currentTarget.blur();
if (!ref.current?.outerHTML) return;
const blob = new Blob([ref.current.outerHTML]);
saveAs(blob, `${name}${weight === "regular" ? "" : `-${weight}`}.svg`);
};
const handleDownloadPNG = async (
event: React.MouseEvent<HTMLButtonElement, MouseEvent>
) => {
event.currentTarget.blur();
if (!ref.current?.outerHTML) return;
Svg2Png.save(
ref.current,
`${name}${weight === "regular" ? "" : `-${weight}`}.png`,
{ scaleX: 2.667, scaleY: 2.667 }
);
};
return (
<motion.section
className="info-box"
animate="open"
exit="collapsed"
variants={panelVariants}
style={{
order: index + (spans - (index % spans)),
color: isDark ? "white" : "black",
}}
>
<motion.div
initial="collapsed"
animate="open"
exit="collapsed"
variants={contentVariants}
className="icon-preview"
>
<Icon ref={ref} color={color} weight={weight} size={192} />
<p>{name}</p>
<TagCloud
name={name}
tags={Array.from(
new Set<string>([...categories, ...name.split("-"), ...tags])
)}
isDark={isDark}
/>
</motion.div>
<motion.div
initial="collapsed"
animate="open"
exit="collapsed"
variants={contentVariants}
className="icon-usage"
>
<div className="snippet">
React
<pre tabIndex={0}>
<span>{snippets.react}</span>
<button
title="Copy snippet"
onClick={(e) => handleCopySnippet(e, "react")}
>
{copied === "react" ? (
<CheckCircle size={24} color={successColor} weight="fill" />
) : (
<Copy size={24} color={buttonColor} weight="fill" />
)}
</button>
</pre>
</div>
<div className="snippet">
Vue
<pre tabIndex={0}>
<span>{snippets.vue}</span>
<button
title="Copy snippet"
onClick={(e) => handleCopySnippet(e, "vue")}
>
{copied === "vue" ? (
<CheckCircle size={24} color={successColor} weight="fill" />
) : (
<Copy size={24} color={buttonColor} weight="fill" />
)}
</button>
</pre>
</div>
<div className="snippet">
HTML/CSS
<pre tabIndex={0} style={snippetButtonStyle}>
<span>{snippets.html}</span>
<button
title="Copy snippet"
onClick={(e) => handleCopySnippet(e, "html")}
disabled={weight === "duotone"}
style={snippetButtonStyle}
>
{copied === "html" ? (
<CheckCircle size={24} color={successColor} weight="fill" />
) : (
<Copy
size={24}
color={snippetButtonStyle.color}
weight="fill"
/>
)}
</button>
</pre>
</div>
<div className="button-row">
<button style={buttonBarStyle} onClick={handleDownloadPNG}>
<Download size={32} color="currentColor" weight="fill" /> Download
PNG
</button>
<button style={buttonBarStyle} onClick={handleDownloadSVG}>
<Download size={32} color="currentColor" weight="fill" /> Download
SVG
</button>
<button style={buttonBarStyle} onClick={handleCopySVG}>
{copied === "svg" ? (
<CheckCircle size={32} color={successColor} weight="fill" />
) : (
<Copy size={32} color="currentColor" weight="fill" />
)}
{copied === "svg" ? "Copied!" : "Copy SVG"}
</button>
</div>
</motion.div>
<motion.span
initial="collapsed"
animate="open"
exit="collapsed"
variants={contentVariants}
title="Close"
>
<X
className="close-icon"
tabIndex={0}
color={buttonBarStyle.color}
size={32}
weight="fill"
onClick={() => setOpen(false)}
onKeyDown={(e) => {
e.key === "Enter" && setOpen(false);
}}
/>
</motion.span>
</motion.section>
);
};
export default DetailsPanel;

View File

@@ -1,7 +1,11 @@
.grid-container {
position: relative;
padding: 32px 16px;
min-height: 80vh;
/* min-height: 80vh; */
z-index: 1;
content-visibility: auto;
color: var(--foreground);
background-color: var(--background);
}
.grid {
@@ -26,22 +30,21 @@
-webkit-user-select: none;
user-select: none;
cursor: pointer;
/* transition: background-color 100ms ease; */
}
.grid-item:hover {
background-color: rgba(163, 159, 171, 0.1);
background-color: var(--translucent);
}
.grid-item:focus {
outline: none;
border: 2px solid rgba(163, 159, 171, 0.1);
border: 2px solid var(--translucent);
}
.grid-item p {
font-size: 12px;
line-height: 16px;
color: #86838b;
color: var(--neutral);
margin-top: 12px;
text-align: center;
}
@@ -64,59 +67,19 @@
}
}
.info-box {
position: relative;
display: flex;
width: 100%;
height: 0px;
margin: 0 4px;
border-radius: 16px;
background-color: rgba(163, 159, 171, 0.1);
}
@media screen and (max-width: 1023px) {
.icon-preview {
display: none !important;
}
.icon-usage {
padding-left: 10% !important;
}
.snippet pre {
padding: 12px 8px 12px 20px;
}
}
.icon-preview {
width: 30%;
display: flex;
text-align: center;
flex-direction: column;
align-items: center;
margin-top: 72px;
}
.icon-preview p {
margin: 0;
font-size: 12px;
line-height: 16px;
}
.icon-usage {
flex: 1;
padding: 56px 10% 56px 0;
.versioning {
margin-top: 2px;
opacity: 0.6;
}
.snippet {
margin-bottom: 24px;
width: 100%;
}
.snippet pre {
display: flex;
align-items: center;
align-items: flex-start;
text-overflow: ellipsis;
color: black;
-moz-user-select: all;
-webkit-user-select: all;
user-select: all;
@@ -171,12 +134,42 @@
.close-icon {
position: absolute;
top: 24px;
right: 24px;
top: 12px;
right: 12px;
text-align: end;
cursor: pointer;
}
.close-button {
color: inherit;
background: transparent;
height: unset !important;
padding: 0 !important;
margin: 0 !important;
border-radius: 48px !important;
position: absolute;
top: -14px;
right: -18px;
text-align: end;
cursor: pointer;
}
.close-button::before {
content: "";
background: var(--background);
position: absolute;
width: 18px;
height: 18px;
top: 5px;
left: 5px;
border-radius: 50%;
z-index: -1;
}
.close-button:active {
opacity: 0.7;
}
.empty-list {
display: flex;
flex-direction: column;
@@ -201,3 +194,66 @@
position: relative;
top: -96px;
}
aside.detail-footer {
position: sticky;
bottom: 16px;
margin: auto;
max-width: 1120px;
display: grid;
grid-template-columns: 232px 1fr;
gap: 24px;
padding: 12px 24px;
height: 136px;
}
figure {
margin: 0;
display: grid;
grid-template-columns: 64px 1fr;
gap: 24px;
align-items: center;
}
figcaption {
display: flex;
flex-direction: column;
font-size: 14px;
}
figcaption > p {
margin: 0;
}
.detail-preview {
display: flex;
flex-direction: column;
justify-content: center;
gap: 24px;
}
.detail-actions {
display: flex;
flex-direction: row;
align-items: center;
gap: 8px;
}
@media screen and (max-width: 719px) {
.close-button {
top: 4px;
right: 12px;
}
aside.detail-footer {
top: 16px;
bottom: -4px;
display: flex;
flex-direction: column;
height: 440px;
}
}
.action-button svg {
margin-right: 6px;
}

View File

@@ -1,30 +1,39 @@
import React, { useRef, useEffect } from "react";
import { useRef, useEffect } from "react";
import { useRecoilValue } from "recoil";
import { motion, useAnimation } from "framer-motion";
import { IconContext } from "phosphor-react";
import { IconContext } from "@phosphor-icons/react";
import { iconWeightAtom, iconSizeAtom, iconColorAtom } from "../../state/atoms";
import {
iconWeightAtom,
iconSizeAtom,
iconColorAtom,
filteredQueryResultsSelector,
isDarkThemeSelector,
} from "../../state/selectors";
import useGridSpans from "../../hooks/useGridSpans";
} from "@/state";
import Notice from "@/components/Notice";
import DetailFooter from "./DetailFooter";
import IconGridItem from "./IconGridItem";
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 = {};
const IconGrid: React.FC<IconGridProps> = () => {
const IconGrid = (_: IconGridProps) => {
const weight = useRecoilValue(iconWeightAtom);
const size = useRecoilValue(iconSizeAtom);
const color = useRecoilValue(iconColorAtom);
const isDark = useRecoilValue(isDarkThemeSelector);
const spans = useGridSpans();
const filteredQueryResults = useRecoilValue(filteredQueryResultsSelector);
const originOffset = useRef({ top: 0, left: 0 });
@@ -38,34 +47,26 @@ const IconGrid: React.FC<IconGridProps> = () => {
return (
<Notice>
<span>Try searching a category or keyword:</span>
<TagCloud name="empty-state" isDark={isDark} tags={defaultSearchTags} />
<TagCloud name="empty-state" tags={defaultSearchTags} />
</Notice>
);
return (
<IconContext.Provider value={{ weight, size, color, mirrored: false }}>
<div
className="grid-container"
style={{ backgroundColor: isDark ? "#35313D" : "" }}
>
<div className="grid-container">
<i id="beacon" className="beacon" />
<motion.div
className="grid"
initial="hidden"
animate={controls}
variants={{}}
>
<motion.div className="grid" initial="hidden" animate={controls}>
{filteredQueryResults.map((iconEntry, index) => (
<IconGridItem
key={index}
index={index}
spans={spans}
isDark={isDark}
entry={iconEntry}
originOffset={originOffset}
/>
))}
</motion.div>
<DetailFooter />
</div>
</IconContext.Provider>
);

View File

@@ -1,19 +1,18 @@
import React, {
import {
useRef,
useLayoutEffect,
useEffect,
MutableRefObject,
HTMLAttributes,
} from "react";
import { useRecoilState } from "recoil";
import { motion, AnimatePresence } from "framer-motion";
import { motion } from "framer-motion";
import { iconPreviewOpenAtom } from "../../state/atoms";
import DetailsPanel from "./DetailsPanel";
import { IconEntry } from "../../lib";
import { IconEntry } from "@/lib";
import { selectionEntryAtom } from "@/state";
interface IconGridItemProps {
interface IconGridItemProps extends HTMLAttributes<HTMLDivElement> {
index: number;
spans: number;
isDark: boolean;
entry: IconEntry;
originOffset: MutableRefObject<{ top: number; left: number }>;
@@ -25,22 +24,24 @@ const delayPerPixel = 0.0004;
const itemVariants = {
hidden: { opacity: 0 },
visible: (delayRef: any) => ({
visible: (delayRef: MutableRefObject<number>) => ({
opacity: 1,
transition: { delay: delayRef.current },
}),
};
const IconGridItem: React.FC<IconGridItemProps> = (props) => {
const { index, originOffset, entry } = props;
const IconGridItem = (props: IconGridItemProps) => {
const { index, originOffset, entry, style } = props;
const { name, Icon } = entry;
const [open, setOpen] = useRecoilState(iconPreviewOpenAtom);
const isOpen = open === name;
const [selection, setSelectionEntry] = useRecoilState(selectionEntryAtom);
const isOpen = selection?.name === 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>();
const handleOpen = () => setOpen(isOpen ? false : name);
const handleOpen = () => setSelectionEntry(isOpen ? null : entry);
// The measurement for all elements happens in the layoutEffect cycle
// This ensures that when we calculate distance in the effect cycle
@@ -74,8 +75,8 @@ const IconGridItem: React.FC<IconGridItemProps> = (props) => {
ref={ref}
tabIndex={0}
style={{
order: index,
backgroundColor: isOpen ? "rgba(163, 159, 171, 0.1)" : undefined,
...style,
backgroundColor: isOpen ? "var(--translucent)" : undefined,
}}
custom={delayRef}
transition={transition}
@@ -84,11 +85,12 @@ 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>
</>
);
};

View File

@@ -2,31 +2,27 @@
display: flex;
flex-wrap: wrap;
justify-content: center;
padding: 24px;
}
button.tag-button {
margin: 4px;
border-radius: 4px;
background-color: rgba(194, 186, 196, 0.25);
background-color: var(--sheer);
outline: none;
cursor: pointer;
transition: background-color 200ms ease, box-shadow 200ms ease;
color: var(--foreground);
}
button.tag-button:hover {
background-color: rgba(194, 186, 196, 0.7);
background-color: var(--soft);
}
button.tag-button:focus {
box-shadow: 0 0 0 1px rgba(194, 186, 196, 0.7);
button.tag-button:focus-visible {
box-shadow: 0 0 0 1px var(--soft);
}
.tag-button code {
padding: 4px;
font-size: 12px;
}
.dark {
color: white;
}

View File

@@ -1,23 +1,24 @@
import React, { useCallback } from "react";
import { useCallback } from "react";
import { useSetRecoilState } from "recoil";
import { searchQueryAtom } from "../../state/atoms";
import { useMediaQuery } from "@/hooks";
import { searchQueryAtom } from "@/state";
import "./TagCloud.css";
interface TagCloudProps {
name: string;
tags: string[];
isDark: boolean;
}
const TagCloud: React.FC<TagCloudProps> = ({ name, tags, isDark }) => {
const TagCloud = ({ name, tags }: TagCloudProps) => {
const isMobile = useMediaQuery("(max-width: 719px)");
const setQuery = useSetRecoilState(searchQueryAtom);
const handleTagClick = useCallback(
(tag: string) => {
setQuery(tag);
document.getElementById("search-input")?.focus();
!isMobile && document.getElementById("search-input")?.focus();
},
[setQuery]
[setQuery, isMobile]
);
return (
@@ -28,7 +29,9 @@ const TagCloud: React.FC<TagCloudProps> = ({ name, tags, isDark }) => {
className="tag-button"
onClick={() => void handleTagClick(tag)}
>
<code className={`${isDark ? "dark" : ""}`}>{tag}</code>
<code>{tag}</code>
{tag === "*new*" && <span className="badge new"></span>}
{tag === "*updated*" && <span className="badge updated"></span>}
</button>
))}
</div>

View File

@@ -0,0 +1 @@
export { default } from "./IconGrid";

View File

@@ -23,6 +23,7 @@
a.nav-link {
text-decoration: none;
position: relative;
cursor: pointer;
color: black;
}

View File

@@ -1,21 +1,20 @@
import React from "react";
import { OutboundLink } from "react-ga";
import { ArrowElbowDownRight } from "phosphor-react";
import { ArrowElbowDownRight } from "@phosphor-icons/react";
import { iconCount } from "../../lib/icons";
import { iconCount } from "@/lib/icons";
import OutboundLink from "@/components/OutboundLink";
import "./Links.css";
interface LinksProps {}
const Links: React.FC<LinksProps> = () => {
const Links = (_: LinksProps) => {
return (
<div className="links">
<div>
<ArrowElbowDownRight size={24} />
<OutboundLink
className="nav-link"
to="https://phosphoricons.com/assets/phosphor-icons.zip"
href="https://phosphoricons.com/assets/phosphor-icons.zip"
eventLabel="Download all"
download
type="application/zip"
@@ -23,67 +22,71 @@ const Links: React.FC<LinksProps> = () => {
Download all ({iconCount})
</OutboundLink>
</div>
<div>
<ArrowElbowDownRight size={24} />
<span>
<OutboundLink
className="nav-link"
to="https://www.figma.com/community/file/903830135544202908/Phosphor-Icons"
href="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"
href="https://www.figma.com/community/plugin/898620911119764089/Phosphor-Icons"
eventLabel="Figma plugin"
>
plugin
</OutboundLink>
</span>
</div>
<div>
<ArrowElbowDownRight size={24} />
<a
className="nav-link"
href="https://github.com/phosphor-icons/phosphor-home/issues"
<OutboundLink
href="https://phosphoricons.com/assets/phosphor-icons.sketchplugin.zip"
eventLabel="Download sketch plugin"
download
type="application/zip"
>
Request an icon
</a>
Sketch plugin
</OutboundLink>
</div>
{/* <div>
<div>
<ArrowElbowDownRight size={24} />
<span>
<a className="nav-link" href="https://paypal.me/minoraxis">
<OutboundLink href="https://paypal.me/minoraxis" eventLabel="Donate">
Donate on PayPal
</a>
</OutboundLink>
{" / "}
<a className="nav-link" href="https://patreon.com/phosphoricons">
<OutboundLink
href="https://patreon.com/phosphoricons"
eventLabel="Patreon"
>
Patreon
</a>
</OutboundLink>
</span>
</div> */}
<div>
<ArrowElbowDownRight size={24} />
<a className="nav-link" href="https://paypal.me/minoraxis">
Donate on PayPal
</a>
</div>
<div>
<ArrowElbowDownRight size={24} />
<a className="nav-link" href="https://patreon.com/phosphoricons">
Support us on Patreon
</a>
</div>
<div>
<ArrowElbowDownRight size={24} />
<a
className="nav-link"
href="https://github.com/phosphor-icons/phosphor-home"
<OutboundLink
href="https://github.com/phosphor-icons/homepage"
eventLabel="GitHub"
>
GitHub
</a>
</OutboundLink>
</div>
<div>
<ArrowElbowDownRight size={24} />
<OutboundLink
href="https://github.com/phosphor-icons/homepage/issues"
eventLabel="Request"
>
Request an icon
</OutboundLink>
</div>
</div>
);

View File

@@ -0,0 +1 @@
export { default } from "./Links";

View File

@@ -1,22 +1,21 @@
import React from "react";
import { ReactNode } from "react";
import { motion } from "framer-motion";
import { useRecoilValue } from "recoil";
import { HourglassMedium, Question, SmileyXEyes } from "@phosphor-icons/react";
import { isDarkThemeSelector } from "../../state/selectors";
import { searchQueryAtom } from "../../state/atoms";
import { HourglassMedium, Question, SmileyXEyes } from "phosphor-react";
import { searchQueryAtom } from "@/state";
interface NoticeProps {
message?: string;
type?: "wait" | "help" | "warn" | "none";
children?: ReactNode;
}
const Notice: React.FC<NoticeProps> = ({ message, type = "warn", children }) => {
const isDark = useRecoilValue(isDarkThemeSelector);
const Notice = ({ message, type = "warn", children }: NoticeProps) => {
const query = useRecoilValue(searchQueryAtom);
return (
<div style={isDark ? { backgroundColor: "#35313D", color: "white" } : {}}>
<div className="primary">
<motion.div
className="empty-list"
initial={{ opacity: 0 }}

View File

@@ -0,0 +1 @@
export { default } from "./Notice";

View File

@@ -0,0 +1,64 @@
import {
DetailedHTMLProps,
AnchorHTMLAttributes,
useCallback,
MouseEventHandler,
} from "react";
import ReactGA from "react-ga4";
import { UaEventOptions } from "react-ga4/types/ga4";
interface OutboundLinkProps
extends DetailedHTMLProps<
AnchorHTMLAttributes<HTMLAnchorElement>,
HTMLAnchorElement
> {
eventLabel: string;
}
const NEWTAB = "_blank";
const MIDDLECLICK = 1;
const DEFAULT_META: UaEventOptions = {
category: "Outbound",
action: "Click",
};
const OutboundLink = ({
eventLabel,
target,
href,
...props
}: OutboundLinkProps) => {
const handleClick: MouseEventHandler<HTMLAnchorElement> = useCallback(
(event) => {
const eventMeta = { ...DEFAULT_META, label: eventLabel };
const sameTarget = target !== NEWTAB;
const normalClick = !(
event.ctrlKey ||
event.shiftKey ||
event.metaKey ||
event.button === MIDDLECLICK
);
if (!!href && sameTarget && normalClick) {
event.preventDefault();
ReactGA.event(eventMeta);
window.location.href = href;
} else {
ReactGA.event(eventMeta);
}
},
[href, eventLabel]
);
return (
<a
{...props}
href={href}
onClick={handleClick}
rel={target === NEWTAB ? "noopener noreferrer" : ""}
className="nav-link"
/>
);
};
export default OutboundLink;

View File

@@ -0,0 +1 @@
export { default } from "./OutboundLink";

View File

@@ -6,7 +6,7 @@
padding: 0 24px;
border-radius: 8px;
color: white;
background-color: rgba(255, 255, 255, 0.05);
background-color: var(--scrim);
}
.search-bar:focus-within {

View File

@@ -1,11 +1,17 @@
import React, { useState, useEffect, useRef, MutableRefObject } from "react";
import {
useState,
useEffect,
useRef,
MutableRefObject,
ReactNode,
} from "react";
import { useRecoilState } from "recoil";
import { useDebounce } from "react-use";
import { useHotkeys } from "react-hotkeys-hook";
import { Command, MagnifyingGlass, X, HourglassHigh } from "phosphor-react";
import ReactGA from "react-ga";
import { Command, MagnifyingGlass, X, HourglassHigh } from "@phosphor-icons/react";
import ReactGA from "react-ga4";
import { searchQueryAtom } from "../../state/atoms";
import { searchQueryAtom } from "@/state";
import "./SearchInput.css";
const apple = /iPhone|iPod|iPad|Macintosh|MacIntel|MacPPC/i;
@@ -16,10 +22,11 @@ const isMobile = mobile.test(window.navigator.userAgent);
type SearchInputProps = {};
const SearchInput: React.FC<SearchInputProps> = () => {
const SearchInput = (_: SearchInputProps) => {
const [value, setValue] = useState<string>("");
const [query, setQuery] = useRecoilState(searchQueryAtom);
const inputRef = useRef<HTMLInputElement>() as MutableRefObject<HTMLInputElement>;
const inputRef =
useRef<HTMLInputElement>() as MutableRefObject<HTMLInputElement>;
useHotkeys("ctrl+k,cmd+k", (e) => {
e.preventDefault();
@@ -77,8 +84,8 @@ const SearchInput: React.FC<SearchInputProps> = () => {
value={value}
placeholder="Search"
onChange={({ currentTarget }) => setValue(currentTarget.value)}
onKeyPress={({ currentTarget, key }) =>
key === "Enter" && currentTarget.blur()
onKeyDown={({ currentTarget, key }) =>
(key === "Enter" || key === "Escape") && currentTarget.blur()
}
/>
{!value && !isMobile && <Keys>{isApple ? <Command /> : "Ctrl + "}K</Keys>}
@@ -93,7 +100,7 @@ const SearchInput: React.FC<SearchInputProps> = () => {
);
};
const Keys: React.FC<{}> = ({ children }) => (
const Keys = ({ children }: { children?: ReactNode }) => (
<div className="keys">{children}</div>
);

View File

@@ -0,0 +1 @@
export { default } from "./SearchInput";

View File

@@ -0,0 +1,17 @@
button.action-button {
background-color: var(--scrim);
color: white;
padding: 8px;
border-radius: 8px;
cursor: pointer;
}
button.action-button:active {
background-color: var(--sheer);
}
@media screen and (max-width: 558px) {
.action-button {
display: none;
}
}

View File

@@ -0,0 +1,62 @@
import { useRecoilValue, useResetRecoilState } from "recoil";
import { ArrowCounterClockwise, CheckCircle, Link } from "@phosphor-icons/react";
import { useTransientState } from "@/hooks";
import {
iconWeightAtom,
iconSizeAtom,
iconColorAtom,
resetSettingsSelector,
} from "@/state";
import "./SettingsActions.css";
const SettingsActions = () => {
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

@@ -0,0 +1 @@
export { default } from "./SettingsActions";

View File

@@ -7,7 +7,7 @@
padding: 0 24px;
color: white;
border-radius: 8px;
background-color: rgba(255, 255, 255, 0.05);
background-color: var(--scrim);
font-family: "Manrope", sans-serif;
font-size: 16px;
}
@@ -68,12 +68,12 @@
outline: none;
width: 24px; /* Set a specific slider handle width */
height: 24px; /* Slider handle height */
box-shadow: 0 0 0 6px rgba(255, 255, 255, 0.2);
box-shadow: 0 0 0 6px var(--sheer);
}
.size-bar input:focus::-webkit-slider-thumb {
outline: none;
width: 24px; /* Set a specific slider handle width */
height: 24px; /* Slider handle height */
box-shadow: 0 0 0 6px rgba(255, 255, 255, 0.2);
box-shadow: 0 0 0 6px var(--sheer);
}

View File

@@ -1,7 +1,7 @@
import React, { useCallback } from "react";
import { useRecoilState } from "recoil";
import { iconSizeAtom } from "../../state/atoms";
import { iconSizeAtom } from "@/state";
import "./SizeInput.css";
type SizeInputProps = {};
@@ -14,7 +14,7 @@ const handleBlur = (event: React.UIEvent<HTMLInputElement>) => {
event.currentTarget.blur();
};
const SizeInput: React.FC<SizeInputProps> = () => {
const SizeInput = (_: SizeInputProps) => {
const [size, setSize] = useRecoilState(iconSizeAtom);
const handleSizeChange = useCallback(

View File

@@ -0,0 +1 @@
export { default } from "./SizeInput";

View File

@@ -1,20 +1,3 @@
/* .style-select {
position: relative;
}
.style-select {
background-color: gold;
border-radius: 24px;
box-shadow: 4px 4px #ccc;
display: none;
}
.style-select option {
background-color: gold;
border-radius: 24px;
display: none;
} */
.react-dropdown-select {
width: 176px !important;
height: 48px !important;
@@ -22,7 +5,7 @@
padding: 0 24px !important;
color: white;
border-radius: 8px !important;
background-color: rgba(255, 255, 255, 0.05);
background-color: var(--scrim);
font-size: 16px;
border: none !important;
}
@@ -50,19 +33,6 @@
box-shadow: none !important;
}
/* .react-dropdown-select-type-single {
height: 100% !important;
} */
/* .react-dropdown-select-clear,
.react-dropdown-select-dropdown-handle {
color: #fff;
} */
/* .react-dropdown-select-option {
border: 1px solid #000;
} */
.react-dropdown-select-item {
color: #333;
height: 40px !important;
@@ -89,25 +59,24 @@
max-height: 300px;
overflow: auto;
z-index: 9;
/* background: rgb(29, 20, 20) !important; */
box-shadow: none;
}
.react-dropdown-select-item {
color: black;
}
.react-dropdown-select-item:hover {
background-color: #ffd171 !important;
background-color: var(--yellow) !important;
}
.react-dropdown-select-item.react-dropdown-select-item-selected,
.react-dropdown-select-item.react-dropdown-select-item-active {
color: black !important;
background-color: #ffd171 !important;
background-color: var(--yellow) !important;
}
.react-dropdown-select-item:focus {
color: black !important;
background-color: #ffd171 !important;
background-color: var(--yellow) !important;
}
.react-dropdown-select-item.react-dropdown-select-item-disabled {

View File

@@ -1,10 +1,11 @@
import React, { useMemo } from "react";
import { useMemo } from "react";
import { useRecoilState } from "recoil";
import Select from "react-dropdown-select";
import { PencilLine } from "phosphor-react";
import { PencilLine } from "@phosphor-icons/react";
import { IconStyle } from "@phosphor-icons/core";
import { iconWeightAtom } from "@/state";
import { iconWeightAtom } from "../../state/atoms";
import { IconStyle } from "../../lib";
import "./StyleInput.css";
type WeightOption = { key: string; value: IconStyle; icon: JSX.Element };
@@ -44,7 +45,7 @@ const options: WeightOption[] = [
type StyleInputProps = {};
const StyleInput: React.FC<StyleInputProps> = () => {
const StyleInput = (_: StyleInputProps) => {
const [style, setStyle] = useRecoilState(iconWeightAtom);
const currentStyle = useMemo(

View File

@@ -0,0 +1 @@
export { default } from "./StyleInput";

View File

@@ -0,0 +1,56 @@
.tabs {
display: flex;
flex-direction: column;
}
.tabs-header {
display: flex;
align-items: center;
gap: 4px;
}
button.tab {
all: unset;
padding: 6px 4px;
font-size: 12px;
text-align: center;
cursor: pointer;
flex: 1;
border-top-left-radius: 8px;
border-top-right-radius: 8px;
z-index: 2;
}
button.tab:focus-visible {
outline: 1px solid currentColor;
}
button.tab:hover:not(.active) {
background-color: var(--sheer);
}
button.tab.active {
background-color: var(--background);
border-bottom: none;
}
.tab-content {
flex: 1;
height: 77px;
max-height: 77px;
padding: 20px 20px 10px;
border-radius: 8px;
background-color: var(--background);
overflow-y: auto;
}
@media screen and (max-width: 719px) {
.tabs {
flex: 1;
}
.tab-content {
height: unset;
max-height: unset;
}
}

View File

@@ -0,0 +1,59 @@
import { CSSProperties, ReactNode, useState } from "react";
import "./Tabs.css";
export type Tab = {
header: ReactNode;
content: ReactNode;
};
type TabsProps = {
tabs: Tab[];
initialIndex?: number;
onTabChange?: (index: number) => void;
};
const contentStyles: Record<string, CSSProperties> = {
activeLeft: { borderTopLeftRadius: 0 },
activeRight: { borderTopRightRadius: 0 },
} as const;
const Tabs = ({ tabs, initialIndex = 0, onTabChange }: TabsProps) => {
const [activeIndex, setActiveIndex] = useState<number>(
!!tabs[initialIndex] ? initialIndex : 0
);
return (
<div className="secondary tabs" tabIndex={0}>
<div className="tabs-header">
{tabs.map((tab, i) => (
<button
key={i}
tabIndex={0}
className={`tab ${activeIndex === i ? "active" : ""}`}
onClick={() => {
setActiveIndex(i);
onTabChange?.(i);
}}
>
{tab.header}
</button>
))}
</div>
<div
className="tab-content"
style={
activeIndex === 0
? contentStyles.activeLeft
: activeIndex === tabs.length - 1
? contentStyles.activeRight
: undefined
}
>
{tabs[activeIndex]?.content}
</div>
</div>
);
};
export default Tabs;

View File

@@ -0,0 +1,2 @@
export { default } from "./Tabs";
export type { Tab } from "./Tabs";

View File

@@ -4,12 +4,12 @@ nav.toolbar {
top: -1px;
padding: 0;
margin: 0;
background-color: #35313d;
z-index: 1;
background-color: var(--eggplant);
z-index: 2;
display: flex;
justify-content: center;
align-items: center;
box-shadow: 0 2px 0 0 rgba(0, 0, 0, 0.15);
box-shadow: 0 2px 0 0 var(--shadow);
}
.toolbar-contents {

View File

@@ -1,10 +1,11 @@
import React from "react";
import StyleInput from "@/components/StyleInput";
import SearchInput from "@/components/SearchInput";
import SizeInput from "@/components/SizeInput";
import ColorInput from "@/components/ColorInput";
import SettingsActions from "@/components/SettingsActions";
import "./Toolbar.css";
import StyleInput from "../StyleInput/StyleInput";
import SearchInput from "../SearchInput/SearchInput";
import SizeInput from "../SizeInput/SizeInput";
import ColorInput from "../ColorInput/ColorInput";
type ToolbarProps = {};
@@ -16,6 +17,7 @@ const Toolbar: React.FC<ToolbarProps> = () => {
<SearchInput />
<SizeInput />
<ColorInput />
<SettingsActions />
</div>
</nav>
);

View File

@@ -0,0 +1 @@
export { default } from "./Toolbar";

13
src/hooks/index.ts Normal file
View File

@@ -0,0 +1,13 @@
export { default as useCSSVariables } from "./useCSSVariables";
export { default as useDebounce } from "./useDebounce";
export { default as useEvent } from "./useEvent";
export { default as useIconParameters } from "./useIconParameters";
export { default as useLocalStorage } from "./useLocalStorage";
export { default as useMediaQuery } from "./useMediaQuery";
export { default as usePersistSettings } from "./usePersistSettings";
export { default as useSessionStorage } from "./useSessionStorage";
export { default as useThrottle } from "./useThrottle";
export { default as useThrottled } from "./useThrottled";
export { default as useTimeoutFn } from "./useTimeoutFn";
export { default as useTransientState } from "./useTransientState";
export { default as useUnmount } from "./useUnmount";

View File

@@ -0,0 +1,40 @@
import { useEffect, useRef } from "react";
type CSSCustomPropertyName = `--${string}`;
type CSSCustomProperties = {
[property: CSSCustomPropertyName]: string | null;
};
function simpleDiff(prev: CSSCustomProperties, next: CSSCustomProperties) {
const merge = { ...prev, ...next };
return Object.entries(merge).reduce<
[property: CSSCustomPropertyName, value: string | null][]
>((acc, [k, val]) => {
let key = k as CSSCustomPropertyName;
if (
!prev[key as CSSCustomPropertyName] ||
prev[key as CSSCustomPropertyName] !== val
) {
acc.push([key, val]);
}
return acc;
}, []);
}
export default function useCSSVariables(properties: CSSCustomProperties) {
const p = useRef<CSSCustomProperties>({});
useEffect(() => {
const diff = simpleDiff(p.current, properties);
if (diff.length > 0) {
diff.forEach(([key, value]) => {
document.documentElement.style.setProperty(key, value);
});
p.current = properties;
}
}, [properties]);
}

45
src/hooks/useEvent.ts Normal file
View File

@@ -0,0 +1,45 @@
import { useEffect } from "react";
export type UseEventTarget = HTMLElement | SVGElement | Document | Window;
export type UseEventMap<E extends UseEventTarget> = E extends HTMLElement
? HTMLElementEventMap
: E extends SVGElement
? SVGElementEventMap
: E extends Document
? DocumentEventMap
: WindowEventMap;
export type UseEventType<E extends UseEventTarget> = keyof UseEventMap<E>;
/**
* Attach event listeners to arbitrary targets, and perform necessary cleanup
* when unmounting. Provides type inference for the listener based on the
* provided event name (currently supports {@link Window}, {@link Document},
* and subclasses of {@link HTMLElement} and {@link SVGElement}).
*
* @param type an {@link https://developer.mozilla.org/en-US/docs/Web/Events#event_listing event type}
* @param listener a callback to be fired on the event
* @param options {@link AddEventListenerOptions}
* @param el the target element to attack the listener. Defaults to
* {@link Document} when omitted.
*/
export default function useEvent<
K extends UseEventType<T>,
M extends UseEventMap<T>,
T extends UseEventTarget = Document
>(
type: K,
listener: (this: T, ev: M[K]) => any,
options?: boolean | AddEventListenerOptions,
el?: T
) {
useEffect(() => {
const target = el ?? document;
// @ts-ignore
target.addEventListener(type, listener, options);
// @ts-ignore
return () => target.removeEventListener(type, listener);
}, [el, type]);
}

View File

@@ -1,18 +0,0 @@
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) / itemWidth
);
};

View File

@@ -2,8 +2,9 @@ import { useEffect } from "react";
import { useSearchParam } from "react-use";
import { useSetRecoilState } from "recoil";
import TinyColor from "tinycolor2";
import { IconStyle } from "@phosphor-icons/core";
import { iconColorAtom, iconWeightAtom, iconSizeAtom } from "../state/atoms";
import { IconStyle } from "../lib";
export default () => {
const weight = useSearchParam("weight")?.replace(/["']/g, "");
@@ -34,4 +35,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,40 @@
import { useCallback, useState, Dispatch, SetStateAction } from "react";
import { STORAGE_KEY } from "@/state";
type Initializer<S> = () => S;
type Setter<S> = (prev: S) => S;
type Action<S> = S | Setter<S> | Initializer<S>;
function expand<S extends object>(action: Action<S>, prev?: S) {
if (typeof action === "function") {
return (action as Setter<S>)(prev!);
} else {
return action;
}
}
export default function useLocalStorage<S extends object>(
key: string,
fallbackState: S | (() => S)
): [S, Dispatch<SetStateAction<S>>, (partial: Partial<S>) => void] {
const [value, setValue] = useState<S>(() => {
let val = localStorage.getItem(STORAGE_KEY + key);
if (val) return JSON.parse(val) as S;
return expand(fallbackState);
});
const set: Dispatch<SetStateAction<S>> = useCallback((val) => {
setValue((prev) => {
const next = expand(val, prev);
localStorage.setItem(STORAGE_KEY + key, JSON.stringify(next));
return next;
});
}, []);
const insert = useCallback(
(partial: Partial<S>) => set((value) => ({ ...value, ...partial })),
[]
);
return [value, set, insert];
}

View File

@@ -0,0 +1,12 @@
import { useMemo, useReducer, Reducer } from "react";
import useEvent from "./useEvent";
const updater: Reducer<number, void> = (s) => (s + 1) % 1_000_000;
export default function useMediaQuery(query: string) {
const mq = useMemo(() => window.matchMedia(query), [query]);
const [, update] = useReducer(updater, 0);
useEvent("resize", update, { passive: true });
return mq.matches;
}

View File

@@ -0,0 +1,23 @@
import { useRecoilValue } from "recoil";
import {
iconWeightAtom,
iconSizeAtom,
iconColorAtom,
STORAGE_KEY,
} from "@/state";
import useDebounce from "./useDebounce";
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(STORAGE_KEY, serializedState);
},
2000,
[weight, size, color]
);
}

View File

@@ -0,0 +1,40 @@
import { useCallback, useState, Dispatch, SetStateAction } from "react";
import { STORAGE_KEY } from "@/state";
type Initializer<S> = () => S;
type Setter<S> = (prev: S) => S;
type Action<S> = S | Setter<S> | Initializer<S>;
function expand<S extends object>(action: Action<S>, prev?: S) {
if (typeof action === "function") {
return (action as Setter<S>)(prev!);
} else {
return action;
}
}
export default function useSessionStorage<S extends object>(
key: string,
fallbackState: S | (() => S)
): [S, Dispatch<SetStateAction<S>>, (partial: Partial<S>) => void] {
const [value, setValue] = useState<S>(() => {
let val = sessionStorage.getItem(STORAGE_KEY + key);
if (val) return JSON.parse(val) as S;
return expand(fallbackState);
});
const set: Dispatch<SetStateAction<S>> = useCallback((val) => {
setValue((prev) => {
const next = expand(val, prev);
sessionStorage.setItem(STORAGE_KEY + key, JSON.stringify(next));
return next;
});
}, []);
const insert = useCallback(
(partial: Partial<S>) => set((value) => ({ ...value, ...partial })),
[]
);
return [value, set, insert];
}

View File

@@ -2,7 +2,10 @@
import { useState, useEffect } from "react";
import { useTimeoutFn } from "react-use";
export default <T>(baseState: T, ms: number = 1000): [T, (transientValue: T) => void] => {
export default <T>(
baseState: T,
ms: number = 1000
): [T, (transientValue: T) => void] => {
const [value, setValue] = useState<T>(baseState);
const [, cancel, restart] = useTimeoutFn(() => setValue(baseState), ms);

View File

@@ -1,27 +1,23 @@
import React from "react";
import ReactDOM from "react-dom";
import { StrictMode } from "react";
import { createRoot } from "react-dom/client";
import { RecoilRoot } from "recoil";
import * as serviceWorker from "./serviceWorker";
import App from "./components/App/App";
import ReactGA from "react-ga";
import App from "./components/App";
import ReactGA from "react-ga4";
ReactGA.initialize("UA-179205759-1", { titleCase: false });
ReactGA.pageview(window.location.pathname);
const GA_MEASUREMENT_ID = 'G-1C1REQCLFB'
ReactGA.initialize(GA_MEASUREMENT_ID);
ReactDOM.render(
<React.StrictMode>
const container = document.getElementById("root");
const root = createRoot(container!);
root.render(
<StrictMode>
<RecoilRoot>
<App />
</RecoilRoot>
</React.StrictMode>,
document.getElementById("root")
</StrictMode>
);
// If you want your app to work offline and load faster, you can change
// unregister() to register() below. Note this comes with some pitfalls.
// Learn more about service workers: https://bit.ly/CRA-PWA
serviceWorker.unregister();
console.log(`
%c sphorphosphor %co%cspho

File diff suppressed because it is too large Load Diff

View File

@@ -1,38 +1,14 @@
import { Icon } from "phosphor-react";
import { Icon } from "@phosphor-icons/react";
import { IconEntry as CoreEntry } from "@phosphor-icons/core";
export enum IconStyle {
THIN = "thin",
LIGHT = "light",
REGULAR = "regular",
BOLD = "bold",
FILL = "fill",
DUOTONE = "duotone",
}
export enum IconCategory {
ARROWS = "arrows",
BRAND = "brands",
COMMERCE = "commerce",
COMMUNICATION = "communications",
DESIGN = "design",
DEVELOPMENT = "technology & development",
OFFICE = "office",
EDITOR = "editor",
FINANCE = "finances",
GAMES = "games",
HEALTH = "health & wellness",
MAP = "maps & travel",
MEDIA = "media",
NATURE = "nature",
OBJECTS = "objects",
PEOPLE = "people",
SYSTEM = "system",
WEATHER = "weather",
}
export interface IconEntry {
name: string;
categories: IconCategory[];
tags: string[];
export interface IconEntry extends CoreEntry {
Icon: Icon;
}
export enum SnippetType {
REACT = "React",
VUE = "Vue",
HTML = "HTML/CSS",
FLUTTER = "Flutter",
ELM = "Elm",
}

View File

@@ -1 +0,0 @@
/// <reference types="react-scripts" />

View File

@@ -1,149 +0,0 @@
// This optional code is used to register a service worker.
// register() is not called by default.
// This lets the app load faster on subsequent visits in production, and gives
// it offline capabilities. However, it also means that developers (and users)
// will only see deployed updates on subsequent visits to a page, after all the
// existing tabs open on the page have been closed, since previously cached
// resources are updated in the background.
// To learn more about the benefits of this model and instructions on how to
// opt-in, read https://bit.ly/CRA-PWA
const isLocalhost = Boolean(
window.location.hostname === 'localhost' ||
// [::1] is the IPv6 localhost address.
window.location.hostname === '[::1]' ||
// 127.0.0.0/8 are considered localhost for IPv4.
window.location.hostname.match(
/^127(?:\.(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)){3}$/
)
);
type Config = {
onSuccess?: (registration: ServiceWorkerRegistration) => void;
onUpdate?: (registration: ServiceWorkerRegistration) => void;
};
export function register(config?: Config) {
if (process.env.NODE_ENV === 'production' && 'serviceWorker' in navigator) {
// The URL constructor is available in all browsers that support SW.
const publicUrl = new URL(
process.env.PUBLIC_URL,
window.location.href
);
if (publicUrl.origin !== window.location.origin) {
// Our service worker won't work if PUBLIC_URL is on a different origin
// from what our page is served on. This might happen if a CDN is used to
// serve assets; see https://github.com/facebook/create-react-app/issues/2374
return;
}
window.addEventListener('load', () => {
const swUrl = `${process.env.PUBLIC_URL}/service-worker.js`;
if (isLocalhost) {
// This is running on localhost. Let's check if a service worker still exists or not.
checkValidServiceWorker(swUrl, config);
// Add some additional logging to localhost, pointing developers to the
// service worker/PWA documentation.
navigator.serviceWorker.ready.then(() => {
console.log(
'This web app is being served cache-first by a service ' +
'worker. To learn more, visit https://bit.ly/CRA-PWA'
);
});
} else {
// Is not localhost. Just register service worker
registerValidSW(swUrl, config);
}
});
}
}
function registerValidSW(swUrl: string, config?: Config) {
navigator.serviceWorker
.register(swUrl)
.then(registration => {
registration.onupdatefound = () => {
const installingWorker = registration.installing;
if (installingWorker == null) {
return;
}
installingWorker.onstatechange = () => {
if (installingWorker.state === 'installed') {
if (navigator.serviceWorker.controller) {
// At this point, the updated precached content has been fetched,
// but the previous service worker will still serve the older
// content until all client tabs are closed.
console.log(
'New content is available and will be used when all ' +
'tabs for this page are closed. See https://bit.ly/CRA-PWA.'
);
// Execute callback
if (config && config.onUpdate) {
config.onUpdate(registration);
}
} else {
// At this point, everything has been precached.
// It's the perfect time to display a
// "Content is cached for offline use." message.
console.log('Content is cached for offline use.');
// Execute callback
if (config && config.onSuccess) {
config.onSuccess(registration);
}
}
}
};
};
})
.catch(error => {
console.error('Error during service worker registration:', error);
});
}
function checkValidServiceWorker(swUrl: string, config?: Config) {
// Check if the service worker can be found. If it can't reload the page.
fetch(swUrl, {
headers: { 'Service-Worker': 'script' }
})
.then(response => {
// Ensure service worker exists, and that we really are getting a JS file.
const contentType = response.headers.get('content-type');
if (
response.status === 404 ||
(contentType != null && contentType.indexOf('javascript') === -1)
) {
// No service worker found. Probably a different app. Reload the page.
navigator.serviceWorker.ready.then(registration => {
registration.unregister().then(() => {
window.location.reload();
});
});
} else {
// Service worker found. Proceed as normal.
registerValidSW(swUrl, config);
}
})
.catch(() => {
console.log(
'No internet connection found. App is running in offline mode.'
);
});
}
export function unregister() {
if ('serviceWorker' in navigator) {
navigator.serviceWorker.ready
.then(registration => {
registration.unregister();
})
.catch(error => {
console.error(error.message);
});
}
}

View File

@@ -1,5 +0,0 @@
// jest-dom adds custom jest matchers for asserting on DOM nodes.
// allows you to do things like:
// expect(element).toHaveTextContent(/react/i)
// learn more: https://github.com/testing-library/jest-dom
import '@testing-library/jest-dom/extend-expect';

View File

@@ -1,27 +1,33 @@
import { atom } from "recoil";
import { IconStyle } from "../lib";
import { IconStyle } from "@phosphor-icons/core";
import { IconEntry } from "@/lib";
export const searchQueryAtom = atom<string>({
key: "searchQueryAtom",
key: "searchQuery",
default: "",
});
export const iconWeightAtom = atom<IconStyle>({
key: "iconWeightAtom",
key: "iconWeight",
default: IconStyle.REGULAR,
});
export const iconSizeAtom = atom<number>({
key: "iconSizeAtom",
default: 48,
key: "iconSize",
default: 32,
});
export const iconColorAtom = atom<string>({
key: "iconColorAtom",
key: "iconColor",
default: "#000000",
});
export const iconPreviewOpenAtom = atom<string | false>({
key: "iconPreviewOpenAtom",
key: "iconPreviewOpen",
default: false,
});
export const selectionEntryAtom = atom<IconEntry | null>({
key: "selectionEntry",
default: null,
});

4
src/state/index.ts Normal file
View File

@@ -0,0 +1,4 @@
export * from "./atoms";
export * from "./selectors";
export const STORAGE_KEY = "__phosphor_settings__";

View File

@@ -1,10 +1,16 @@
import { selector, selectorFamily } from "recoil";
import TinyColor from "tinycolor2";
import Fuse from "fuse.js";
import { IconCategory } from "@phosphor-icons/core";
import { searchQueryAtom, iconColorAtom } from "./atoms";
import { IconEntry, IconCategory } from "../lib";
import { icons } from "../lib/icons";
import {
searchQueryAtom,
iconWeightAtom,
iconSizeAtom,
iconColorAtom,
} from "./atoms";
import { IconEntry } from "@/lib";
import { icons } from "@/lib/icons";
const fuse = new Fuse(icons, {
keys: [{ name: "name", weight: 4 }, "tags", "categories"],
@@ -14,7 +20,7 @@ const fuse = new Fuse(icons, {
});
export const filteredQueryResultsSelector = selector<ReadonlyArray<IconEntry>>({
key: "filteredQueryResultsSelector",
key: "filteredQueryResults",
get: ({ get }) => {
const query = get(searchQueryAtom).trim().toLowerCase();
if (!query) return icons;
@@ -30,7 +36,7 @@ type CategorizedIcons = Partial<Record<IconCategory, IconEntry[]>>;
export const categorizedQueryResultsSelector = selector<
Readonly<CategorizedIcons>
>({
key: "categorizedQueryResultsSelector",
key: "categorizedQueryResults",
get: ({ get }) => {
const filteredResults = get(filteredQueryResultsSelector);
return new Promise((resolve) =>
@@ -51,8 +57,10 @@ export const singleCategoryQueryResultsSelector = selectorFamily<
ReadonlyArray<IconEntry>,
IconCategory
>({
key: "singleCategoryQueryResultsSelector",
get: (category: IconCategory) => ({ get }) => {
key: "singleCategoryQueryResults",
get:
(category: IconCategory) =>
({ get }) => {
const filteredResults = get(filteredQueryResultsSelector);
return new Promise((resolve) =>
resolve(
@@ -63,6 +71,16 @@ export const singleCategoryQueryResultsSelector = selectorFamily<
});
export const isDarkThemeSelector = selector<boolean>({
key: "isDarkThemeSelector",
key: "isDarkTheme",
get: ({ get }) => TinyColor(get(iconColorAtom)).isLight(),
});
export const resetSettingsSelector = selector<null>({
key: "resetSettings",
get: () => null,
set: ({ reset }) => {
reset(iconWeightAtom);
reset(iconSizeAtom);
reset(iconColorAtom);
},
});

61
src/utils/index.ts Normal file
View File

@@ -0,0 +1,61 @@
import { IconStyle } from "@phosphor-icons/core";
import { SnippetType } from "@/lib";
export function getCodeSnippets({
name,
displayName,
weight,
size,
color,
}: {
name: string;
displayName: string;
weight: IconStyle;
size: number;
color: string;
}): Record<SnippetType, string> {
const isDefaultWeight = weight === "regular";
const isDefaultColor = color === "#000000";
const elmName = displayName.replace(/^\w/, (c) => c.toLowerCase());
const elmWeight = weight.replace(/^\w/, (c) => c.toUpperCase());
return {
[SnippetType.HTML]: `<i class="ph${
isDefaultWeight ? "" : `-${weight}`
} ph-${name}"></i>`,
[SnippetType.REACT]: `<${displayName} size={${size}} ${
!isDefaultColor ? `color="${color}" ` : ""
}${isDefaultWeight ? "" : `weight="${weight}" `}/>`,
[SnippetType.VUE]: `<ph${displayName
.replace(/([a-z0-9]|(?=[A-Z]))([A-Z])/g, "$1-$2")
.toLowerCase()} :size="${size}" ${
!isDefaultColor ? `color="${color}" ` : ""
}${isDefaultWeight ? "" : `weight="${weight}" `}/>`,
[SnippetType.FLUTTER]: `Icon(\n PhosphorIcons.${displayName.replace(
/^\w/,
(c) => c.toLowerCase()
)}${
isDefaultWeight ? "" : weight.replace(/^\w/, (c) => c.toUpperCase())
},\n size: ${size.toFixed(1)},\n${
!isDefaultColor ? ` color: Color(0xff${color.replace("#", "")}),\n` : ""
})`,
[SnippetType.ELM]: `Phosphor.${elmName}${
isDefaultWeight ? "" : " " + elmWeight
}
|> withSize ${size}
|> withSizeUnit "px"
|> toHtml []`,
};
}
export function supportsWeight({
type,
weight,
}: {
type: SnippetType;
weight: IconStyle;
}): boolean {
if (type !== SnippetType.FLUTTER) return true;
return weight !== IconStyle.DUOTONE;
}

1
src/vite-env.d.ts vendored Normal file
View File

@@ -0,0 +1 @@
/// <reference types="vite-plugin-svgr/client" />

View File

@@ -1,23 +1,22 @@
{
"compilerOptions": {
"target": "es5",
"lib": [
"es6",
"dom",
"dom.iterable",
"esnext"
],
"baseUrl": "./",
"paths": {
"@/*": ["./src/*"]
},
"lib": ["DOM", "DOM.Iterable", "ESNext"],
"target": "ESNext",
"allowJs": true,
"skipLibCheck": true,
"esModuleInterop": true,
"allowSyntheticDefaultImports": true,
"strict": true,
"forceConsistentCasingInFileNames": true,
"module": "esnext",
"module": "ESNext",
"moduleResolution": "node",
"resolveJsonModule": true,
"isolatedModules": true,
"jsx": "react",
"jsx": "react-jsx",
"sourceMap": true,
"declaration": true,
"noUnusedLocals": true,
@@ -26,11 +25,6 @@
"noFallthroughCasesInSwitch": true,
"noEmit": true
},
"include": [
"src"
],
"exclude": [
"node_modules",
"build"
]
"include": ["src"],
"exclude": ["node_modules", "build"]
}

15
vite.config.ts Normal file
View File

@@ -0,0 +1,15 @@
import path from "path";
import { defineConfig } from "vite";
import react from "@vitejs/plugin-react";
import svgr from 'vite-plugin-svgr'
// https://vitejs.dev/config/
export default defineConfig({
plugins: [react(), svgr()],
resolve: {
alias: {
"~": path.resolve(__dirname, "./public"),
"@": path.resolve(__dirname, "./src"),
},
},
});