Compare commits
5 Commits
tf/unplugi
...
master
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
1ac0502868 | ||
|
|
9b2c2ffea2 | ||
|
|
e90584e282 | ||
|
|
ef83bdfb37 | ||
|
|
7f613234e5 |
@@ -17,7 +17,6 @@ jobs:
|
||||
- name: Install pnpm
|
||||
uses: pnpm/action-setup@v4
|
||||
with:
|
||||
version: 9
|
||||
run_install: false
|
||||
|
||||
- name: Install Node.js
|
||||
|
||||
1
.github/workflows/dreamhost-static.yaml
vendored
1
.github/workflows/dreamhost-static.yaml
vendored
@@ -19,7 +19,6 @@ jobs:
|
||||
- name: Install pnpm
|
||||
uses: pnpm/action-setup@v4
|
||||
with:
|
||||
version: 9
|
||||
run_install: false
|
||||
|
||||
- name: Install Node.js
|
||||
|
||||
2
.github/workflows/sync-docs.yaml
vendored
2
.github/workflows/sync-docs.yaml
vendored
@@ -44,7 +44,7 @@ jobs:
|
||||
- name: Install pnpm
|
||||
uses: pnpm/action-setup@v4
|
||||
with:
|
||||
version: 9
|
||||
version: 10
|
||||
run_install: false
|
||||
|
||||
- name: Install Node.js
|
||||
|
||||
10
package.json
10
package.json
@@ -20,7 +20,7 @@
|
||||
],
|
||||
"repository": "github:phosphor-icons/homepage",
|
||||
"private": true,
|
||||
"packageManager": "pnpm@9",
|
||||
"packageManager": "pnpm@10.6.3",
|
||||
"scripts": {
|
||||
"dev": "vite",
|
||||
"build": "tsc && vite build",
|
||||
@@ -31,8 +31,7 @@
|
||||
},
|
||||
"dependencies": {
|
||||
"@phosphor-icons/core": "^2.1.1",
|
||||
"@phosphor-icons/react": "^2.1.4",
|
||||
"@recoiljs/refine": "^0.1.1",
|
||||
"@phosphor-icons/react": "^2.1.8",
|
||||
"file-saver": "^2.0.2",
|
||||
"framer-motion": "^10.17.12",
|
||||
"fuse.js": "^6.4.1",
|
||||
@@ -41,10 +40,9 @@
|
||||
"react-dropdown-select": "^4.4.2",
|
||||
"react-ga4": "^2.1.0",
|
||||
"react-hotkeys-hook": "^4.4.3",
|
||||
"recoil": "^0.7.7",
|
||||
"recoil-sync": "^0.2.0",
|
||||
"svg2png-converter": "^1.0.2",
|
||||
"tinycolor2": "^1.4.2"
|
||||
"tinycolor2": "^1.4.2",
|
||||
"zustand": "^5.0.4"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/file-saver": "^2.0.5",
|
||||
|
||||
132
pnpm-lock.yaml
generated
132
pnpm-lock.yaml
generated
@@ -12,17 +12,14 @@ importers:
|
||||
specifier: ^2.1.1
|
||||
version: 2.1.1
|
||||
'@phosphor-icons/react':
|
||||
specifier: ^2.1.4
|
||||
version: 2.1.4(react-dom@18.2.0)(react@18.2.0)
|
||||
'@recoiljs/refine':
|
||||
specifier: ^0.1.1
|
||||
version: 0.1.1
|
||||
specifier: ^2.1.8
|
||||
version: 2.1.8(react-dom@18.2.0(react@18.2.0))(react@18.2.0)
|
||||
file-saver:
|
||||
specifier: ^2.0.2
|
||||
version: 2.0.5
|
||||
framer-motion:
|
||||
specifier: ^10.17.12
|
||||
version: 10.17.12(react-dom@18.2.0)(react@18.2.0)
|
||||
version: 10.17.12(react-dom@18.2.0(react@18.2.0))(react@18.2.0)
|
||||
fuse.js:
|
||||
specifier: ^6.4.1
|
||||
version: 6.6.2
|
||||
@@ -34,25 +31,22 @@ importers:
|
||||
version: 18.2.0(react@18.2.0)
|
||||
react-dropdown-select:
|
||||
specifier: ^4.4.2
|
||||
version: 4.11.0(@types/react@18.2.47)(prop-types@15.8.1)(react-dom@18.2.0)(react@18.2.0)
|
||||
version: 4.11.0(@types/react@18.2.47)(prop-types@15.8.1)(react-dom@18.2.0(react@18.2.0))(react@18.2.0)
|
||||
react-ga4:
|
||||
specifier: ^2.1.0
|
||||
version: 2.1.0
|
||||
react-hotkeys-hook:
|
||||
specifier: ^4.4.3
|
||||
version: 4.4.3(react-dom@18.2.0)(react@18.2.0)
|
||||
recoil:
|
||||
specifier: ^0.7.7
|
||||
version: 0.7.7(react-dom@18.2.0)(react@18.2.0)
|
||||
recoil-sync:
|
||||
specifier: ^0.2.0
|
||||
version: 0.2.0(recoil@0.7.7)
|
||||
version: 4.4.3(react-dom@18.2.0(react@18.2.0))(react@18.2.0)
|
||||
svg2png-converter:
|
||||
specifier: ^1.0.2
|
||||
version: 1.0.2
|
||||
tinycolor2:
|
||||
specifier: ^1.4.2
|
||||
version: 1.6.0
|
||||
zustand:
|
||||
specifier: ^5.0.4
|
||||
version: 5.0.4(@types/react@18.2.47)(react@18.2.0)
|
||||
devDependencies:
|
||||
'@types/file-saver':
|
||||
specifier: ^2.0.5
|
||||
@@ -71,7 +65,7 @@ importers:
|
||||
version: 1.4.6
|
||||
'@vitejs/plugin-react':
|
||||
specifier: ^3.1.0
|
||||
version: 3.1.0(vite@4.5.2)
|
||||
version: 3.1.0(vite@4.5.2(@types/node@18.19.6))
|
||||
iconjar-exporter:
|
||||
specifier: ^1.0.10
|
||||
version: 1.0.10
|
||||
@@ -89,7 +83,7 @@ importers:
|
||||
version: 4.5.2(@types/node@18.19.6)
|
||||
vite-plugin-svgr:
|
||||
specifier: ^2.4.0
|
||||
version: 2.4.0(vite@4.5.2)
|
||||
version: 2.4.0(rollup@3.29.4)(vite@4.5.2(@types/node@18.19.6))
|
||||
|
||||
packages:
|
||||
|
||||
@@ -555,16 +549,13 @@ packages:
|
||||
'@phosphor-icons/core@2.1.1':
|
||||
resolution: {integrity: sha512-v4ARvrip4qBCImOE5rmPUylOEK4iiED9ZyKjcvzuezqMaiRASCHKcRIuvvxL/twvLpkfnEODCOJp5dM4eZilxQ==}
|
||||
|
||||
'@phosphor-icons/react@2.1.4':
|
||||
resolution: {integrity: sha512-EeNwgcg1aeK5vG/JYVTullnZzk5zjftOrjLb1iYetyhn5ImFFREc+xG0FAB/dAY277DGDSF8pNdNbqG1SUKIxQ==}
|
||||
'@phosphor-icons/react@2.1.8':
|
||||
resolution: {integrity: sha512-RxJlAkErO+t50DsY82ga9RGOULK6Jux0MdmXqvDjtOzG3PYQFz6rjdUU2q06lPMMbJTT+d+qurKYmF7i2Uv74A==}
|
||||
engines: {node: '>=10'}
|
||||
peerDependencies:
|
||||
react: '>= 16.8'
|
||||
react-dom: '>= 16.8'
|
||||
|
||||
'@recoiljs/refine@0.1.1':
|
||||
resolution: {integrity: sha512-ry02rHswJePYkH1o8K99qL4O6TBntF9/g7W5wXVwaOUrIJEZUGfl/I3+btPXbUgyyEZvNs5xcwvOw13AufmFQw==}
|
||||
|
||||
'@rollup/pluginutils@5.1.0':
|
||||
resolution: {integrity: sha512-XTIWOPPcpvyKI6L1NHo0lFlCyznUEyPmPY1mc3KpPVDYulHSTvyeLNVW00QTLIAFNhR3kYnJTQHeGqU4M3n09g==}
|
||||
engines: {node: '>=14.0.0'}
|
||||
@@ -806,9 +797,6 @@ packages:
|
||||
resolution: {integrity: sha512-WOBp/EEGUiIsJSp7wcv/y6MO+lV9UoncWqxuFfm8eBwzWNgyfBd6Gz+IeKQ9jCmyhoH99g15M3T+QaVHFjizVA==}
|
||||
engines: {node: '>=4'}
|
||||
|
||||
hamt_plus@1.0.2:
|
||||
resolution: {integrity: sha512-t2JXKaehnMb9paaYA7J0BX8QQAY8lwfQ9Gjf4pg/mk4krt+cmwmU652HOoWonf+7+EQV97ARPMhhVgU1ra2GhA==}
|
||||
|
||||
has-flag@3.0.0:
|
||||
resolution: {integrity: sha512-sKJf1+ceQBr4SMkvQnBDNDtf4TXpVhVGateu0t918bl30FnbE2m4vNLX+VWe/dpjlb+HugGYzW7uQXH98HPEYw==}
|
||||
engines: {node: '>=4'}
|
||||
@@ -944,23 +932,6 @@ packages:
|
||||
resolution: {integrity: sha512-/3IjMdb2L9QbBdWiW5e3P2/npwMBaU9mHCSCUzNln0ZCYbcfTsGbTJrU/kGemdH2IWmB2ioZ+zkxtmq6g09fGQ==}
|
||||
engines: {node: '>=0.10.0'}
|
||||
|
||||
recoil-sync@0.2.0:
|
||||
resolution: {integrity: sha512-ZYZM1C4LAhGr3EeMMI5MwT4eaEqsr+ddjB4EwdgN8HXXLmE7P5FVCdFHV3HJtMzxR3Y8sOmJDfN1IPrezwKoRg==}
|
||||
peerDependencies:
|
||||
recoil: '>=0.7.3'
|
||||
|
||||
recoil@0.7.7:
|
||||
resolution: {integrity: sha512-8Og5KPQW9LwC577Vc7Ug2P0vQshkv1y3zG3tSSkWMqkWSwHmE+by06L8JtnGocjW6gcCvfwB3YtrJG6/tWivNQ==}
|
||||
peerDependencies:
|
||||
react: '>=16.13.1'
|
||||
react-dom: '*'
|
||||
react-native: '*'
|
||||
peerDependenciesMeta:
|
||||
react-dom:
|
||||
optional: true
|
||||
react-native:
|
||||
optional: true
|
||||
|
||||
regenerator-runtime@0.14.1:
|
||||
resolution: {integrity: sha512-dYnhHh0nJoMfnkZs6GmmhFknAGRrLznOu5nc9ML+EJxGvrx6H7teuevqVqCuPcPK//3eDrrjQhehXVx9cnkGdw==}
|
||||
|
||||
@@ -1019,10 +990,6 @@ packages:
|
||||
resolution: {integrity: sha512-/OaKK0xYrs3DmxRYqL/yDc+FxFUVYhDlXMhRmv3z915w2HF1tnN1omB354j8VUGO/hbRzyD6Y3sA7v7GS/ceog==}
|
||||
engines: {node: '>=4'}
|
||||
|
||||
transit-js@0.8.874:
|
||||
resolution: {integrity: sha512-IDJJGKRzUbJHmN0P15HBBa05nbKor3r2MmG6aSt0UxXIlJZZKcddTk67/U7WyAeW9Hv/VYI02IqLzolsC4sbPA==}
|
||||
engines: {node: '>= 0.10.0'}
|
||||
|
||||
tslib@2.6.2:
|
||||
resolution: {integrity: sha512-AEYxH93jGFPn/a2iVAwW87VuUIkR1FVUKB77NwMF7nBTDkDrrT/Hpt/IrCJ0QXhW27jTBDcf5ZY7w6RiqTMw2Q==}
|
||||
|
||||
@@ -1085,6 +1052,24 @@ packages:
|
||||
resolution: {integrity: sha512-r3vXyErRCYJ7wg28yvBY5VSoAF8ZvlcW9/BwUzEtUsjvX/DKs24dIkuwjtuprwJJHsbyUbLApepYTR1BN4uHrg==}
|
||||
engines: {node: '>= 6'}
|
||||
|
||||
zustand@5.0.4:
|
||||
resolution: {integrity: sha512-39VFTN5InDtMd28ZhjLyuTnlytDr9HfwO512Ai4I8ZABCoyAj4F1+sr7sD1jP/+p7k77Iko0Pb5NhgBFDCX0kQ==}
|
||||
engines: {node: '>=12.20.0'}
|
||||
peerDependencies:
|
||||
'@types/react': '>=18.0.0'
|
||||
immer: '>=9.0.6'
|
||||
react: '>=18.0.0'
|
||||
use-sync-external-store: '>=1.2.0'
|
||||
peerDependenciesMeta:
|
||||
'@types/react':
|
||||
optional: true
|
||||
immer:
|
||||
optional: true
|
||||
react:
|
||||
optional: true
|
||||
use-sync-external-store:
|
||||
optional: true
|
||||
|
||||
snapshots:
|
||||
|
||||
'@ampproject/remapping@2.2.1':
|
||||
@@ -1280,9 +1265,10 @@ snapshots:
|
||||
'@emotion/use-insertion-effect-with-fallbacks': 1.0.1(react@18.2.0)
|
||||
'@emotion/utils': 1.2.1
|
||||
'@emotion/weak-memoize': 0.3.1
|
||||
'@types/react': 18.2.47
|
||||
hoist-non-react-statics: 3.3.2
|
||||
react: 18.2.0
|
||||
optionalDependencies:
|
||||
'@types/react': 18.2.47
|
||||
|
||||
'@emotion/serialize@1.1.3':
|
||||
dependencies:
|
||||
@@ -1294,7 +1280,7 @@ snapshots:
|
||||
|
||||
'@emotion/sheet@1.2.2': {}
|
||||
|
||||
'@emotion/styled@11.11.0(@emotion/react@11.11.0)(@types/react@18.2.47)(react@18.2.0)':
|
||||
'@emotion/styled@11.11.0(@emotion/react@11.11.0(@types/react@18.2.47)(react@18.2.0))(@types/react@18.2.47)(react@18.2.0)':
|
||||
dependencies:
|
||||
'@babel/runtime': 7.23.8
|
||||
'@emotion/babel-plugin': 11.11.0
|
||||
@@ -1303,8 +1289,9 @@ snapshots:
|
||||
'@emotion/serialize': 1.1.3
|
||||
'@emotion/use-insertion-effect-with-fallbacks': 1.0.1(react@18.2.0)
|
||||
'@emotion/utils': 1.2.1
|
||||
'@types/react': 18.2.47
|
||||
react: 18.2.0
|
||||
optionalDependencies:
|
||||
'@types/react': 18.2.47
|
||||
|
||||
'@emotion/unitless@0.8.1': {}
|
||||
|
||||
@@ -1470,18 +1457,18 @@ snapshots:
|
||||
|
||||
'@phosphor-icons/core@2.1.1': {}
|
||||
|
||||
'@phosphor-icons/react@2.1.4(react-dom@18.2.0)(react@18.2.0)':
|
||||
'@phosphor-icons/react@2.1.8(react-dom@18.2.0(react@18.2.0))(react@18.2.0)':
|
||||
dependencies:
|
||||
react: 18.2.0
|
||||
react-dom: 18.2.0(react@18.2.0)
|
||||
|
||||
'@recoiljs/refine@0.1.1': {}
|
||||
|
||||
'@rollup/pluginutils@5.1.0':
|
||||
'@rollup/pluginutils@5.1.0(rollup@3.29.4)':
|
||||
dependencies:
|
||||
'@types/estree': 1.0.5
|
||||
estree-walker: 2.0.2
|
||||
picomatch: 2.3.1
|
||||
optionalDependencies:
|
||||
rollup: 3.29.4
|
||||
|
||||
'@svgr/babel-plugin-add-jsx-attribute@6.5.1(@babel/core@7.23.7)':
|
||||
dependencies:
|
||||
@@ -1578,7 +1565,7 @@ snapshots:
|
||||
|
||||
'@types/tinycolor2@1.4.6': {}
|
||||
|
||||
'@vitejs/plugin-react@3.1.0(vite@4.5.2)':
|
||||
'@vitejs/plugin-react@3.1.0(vite@4.5.2(@types/node@18.19.6))':
|
||||
dependencies:
|
||||
'@babel/core': 7.23.7
|
||||
'@babel/plugin-transform-react-jsx-self': 7.23.3(@babel/core@7.23.7)
|
||||
@@ -1713,13 +1700,13 @@ snapshots:
|
||||
|
||||
find-root@1.1.0: {}
|
||||
|
||||
framer-motion@10.17.12(react-dom@18.2.0)(react@18.2.0):
|
||||
framer-motion@10.17.12(react-dom@18.2.0(react@18.2.0))(react@18.2.0):
|
||||
dependencies:
|
||||
react: 18.2.0
|
||||
react-dom: 18.2.0(react@18.2.0)
|
||||
tslib: 2.6.2
|
||||
optionalDependencies:
|
||||
'@emotion/is-prop-valid': 0.8.8
|
||||
react: 18.2.0
|
||||
react-dom: 18.2.0(react@18.2.0)
|
||||
|
||||
fsevents@2.3.3:
|
||||
optional: true
|
||||
@@ -1736,8 +1723,6 @@ snapshots:
|
||||
|
||||
globals@11.12.0: {}
|
||||
|
||||
hamt_plus@1.0.2: {}
|
||||
|
||||
has-flag@3.0.0: {}
|
||||
|
||||
hasown@2.0.0:
|
||||
@@ -1830,10 +1815,10 @@ snapshots:
|
||||
react: 18.2.0
|
||||
scheduler: 0.23.0
|
||||
|
||||
react-dropdown-select@4.11.0(@types/react@18.2.47)(prop-types@15.8.1)(react-dom@18.2.0)(react@18.2.0):
|
||||
react-dropdown-select@4.11.0(@types/react@18.2.47)(prop-types@15.8.1)(react-dom@18.2.0(react@18.2.0))(react@18.2.0):
|
||||
dependencies:
|
||||
'@emotion/react': 11.11.0(@types/react@18.2.47)(react@18.2.0)
|
||||
'@emotion/styled': 11.11.0(@emotion/react@11.11.0)(@types/react@18.2.47)(react@18.2.0)
|
||||
'@emotion/styled': 11.11.0(@emotion/react@11.11.0(@types/react@18.2.47)(react@18.2.0))(@types/react@18.2.47)(react@18.2.0)
|
||||
prop-types: 15.8.1
|
||||
react: 18.2.0
|
||||
react-dom: 18.2.0(react@18.2.0)
|
||||
@@ -1842,7 +1827,7 @@ snapshots:
|
||||
|
||||
react-ga4@2.1.0: {}
|
||||
|
||||
react-hotkeys-hook@4.4.3(react-dom@18.2.0)(react@18.2.0):
|
||||
react-hotkeys-hook@4.4.3(react-dom@18.2.0(react@18.2.0))(react@18.2.0):
|
||||
dependencies:
|
||||
react: 18.2.0
|
||||
react-dom: 18.2.0(react@18.2.0)
|
||||
@@ -1855,18 +1840,6 @@ snapshots:
|
||||
dependencies:
|
||||
loose-envify: 1.4.0
|
||||
|
||||
recoil-sync@0.2.0(recoil@0.7.7):
|
||||
dependencies:
|
||||
'@recoiljs/refine': 0.1.1
|
||||
recoil: 0.7.7(react-dom@18.2.0)(react@18.2.0)
|
||||
transit-js: 0.8.874
|
||||
|
||||
recoil@0.7.7(react-dom@18.2.0)(react@18.2.0):
|
||||
dependencies:
|
||||
hamt_plus: 1.0.2
|
||||
react: 18.2.0
|
||||
react-dom: 18.2.0(react@18.2.0)
|
||||
|
||||
regenerator-runtime@0.14.1: {}
|
||||
|
||||
resolve-from@4.0.0: {}
|
||||
@@ -1909,8 +1882,6 @@ snapshots:
|
||||
|
||||
to-fast-properties@2.0.0: {}
|
||||
|
||||
transit-js@0.8.874: {}
|
||||
|
||||
tslib@2.6.2: {}
|
||||
|
||||
tsx@4.7.3:
|
||||
@@ -1930,9 +1901,9 @@ snapshots:
|
||||
escalade: 3.1.1
|
||||
picocolors: 1.0.0
|
||||
|
||||
vite-plugin-svgr@2.4.0(vite@4.5.2):
|
||||
vite-plugin-svgr@2.4.0(rollup@3.29.4)(vite@4.5.2(@types/node@18.19.6)):
|
||||
dependencies:
|
||||
'@rollup/pluginutils': 5.1.0
|
||||
'@rollup/pluginutils': 5.1.0(rollup@3.29.4)
|
||||
'@svgr/core': 6.5.1
|
||||
vite: 4.5.2(@types/node@18.19.6)
|
||||
transitivePeerDependencies:
|
||||
@@ -1941,13 +1912,18 @@ snapshots:
|
||||
|
||||
vite@4.5.2(@types/node@18.19.6):
|
||||
dependencies:
|
||||
'@types/node': 18.19.6
|
||||
esbuild: 0.18.20
|
||||
postcss: 8.4.33
|
||||
rollup: 3.29.4
|
||||
optionalDependencies:
|
||||
'@types/node': 18.19.6
|
||||
fsevents: 2.3.3
|
||||
|
||||
yallist@3.1.1: {}
|
||||
|
||||
yaml@1.10.2: {}
|
||||
|
||||
zustand@5.0.4(@types/react@18.2.47)(react@18.2.0):
|
||||
optionalDependencies:
|
||||
'@types/react': 18.2.47
|
||||
react: 18.2.0
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
import { Fragment, Suspense, useMemo } from "react";
|
||||
import { useRecoilValue } from "recoil";
|
||||
|
||||
import "./App.css";
|
||||
import Header from "@/components/Header";
|
||||
@@ -10,13 +9,13 @@ import ErrorBoundary from "@/components/ErrorBoundary";
|
||||
import Notice from "@/components/Notice";
|
||||
// import Recipes from "@/components/Recipes";
|
||||
import { useCSSVariables } from "@/hooks";
|
||||
import { isDarkThemeSelector } from "@/state";
|
||||
import { ApplicationTheme, useApplicationStore } from "@/state";
|
||||
|
||||
const errorFallback = <Notice message="Search error" />;
|
||||
const waitingFallback = <Notice type="none" message="" />;
|
||||
|
||||
const App: React.FC<any> = () => {
|
||||
const isDark = useRecoilValue(isDarkThemeSelector);
|
||||
const isDark = useApplicationStore.use.applicationTheme() === ApplicationTheme.DARK;
|
||||
|
||||
useCSSVariables(
|
||||
useMemo(
|
||||
|
||||
@@ -33,13 +33,13 @@
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: 20px;
|
||||
max-width: 600px;
|
||||
max-width: 800px;
|
||||
margin: auto;
|
||||
padding: 12px 12px 12px 16px;
|
||||
color: var(--moss);
|
||||
background-color: var(--acid);
|
||||
border: 1px solid var(--moss);
|
||||
border-radius: 32px;
|
||||
border-radius: 16px;
|
||||
filter: drop-shadow(2px 2px 0 var(--moss-shadow));
|
||||
font-family: "IBM Plex Mono";
|
||||
font-size: 14px;
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { ReactNode, Dispatch, SetStateAction } from "react";
|
||||
import { motion, AnimatePresence, Variants } from "framer-motion";
|
||||
import { XCircle } from "@phosphor-icons/react";
|
||||
import { XCircleIcon } from "@phosphor-icons/react";
|
||||
import ReactGA from "react-ga4";
|
||||
|
||||
import { useLocalStorage } from "@/hooks";
|
||||
@@ -44,9 +44,9 @@ const Banner = ({ id, children, onClose }: BannerProps) => {
|
||||
onClose
|
||||
? onClose(setBannerState)
|
||||
: setBannerState((state) => ({
|
||||
...state,
|
||||
seen: { ...state.seen, [id]: true },
|
||||
}));
|
||||
...state,
|
||||
seen: { ...state.seen, [id]: true },
|
||||
}));
|
||||
};
|
||||
|
||||
return (
|
||||
@@ -69,7 +69,7 @@ const Banner = ({ id, children, onClose }: BannerProps) => {
|
||||
e.key === "Enter" && handleClose();
|
||||
}}
|
||||
>
|
||||
<XCircle size={28} weight="regular" />
|
||||
<XCircleIcon size={28} weight="regular" />
|
||||
</button>
|
||||
</div>
|
||||
</motion.aside>
|
||||
|
||||
@@ -1,17 +1,20 @@
|
||||
import { useCallback } from "react";
|
||||
import { useRecoilState, useRecoilValue } from "recoil";
|
||||
import { EyedropperSample } from "@phosphor-icons/react";
|
||||
import { useShallow } from "zustand/react/shallow";
|
||||
import { EyedropperSampleIcon } from "@phosphor-icons/react";
|
||||
|
||||
import { useThrottled } from "@/hooks";
|
||||
import { iconColorAtom, isDarkThemeSelector } from "@/state";
|
||||
import { ApplicationTheme, useApplicationStore } from "@/state";
|
||||
|
||||
import "./ColorInput.css";
|
||||
|
||||
type ColorInputProps = {};
|
||||
|
||||
const ColorInput = (_: ColorInputProps) => {
|
||||
const [color, setColor] = useRecoilState(iconColorAtom);
|
||||
const isDark = useRecoilValue(isDarkThemeSelector);
|
||||
const { color, setColor, theme } = useApplicationStore(useShallow((state) => ({
|
||||
color: state.iconColor,
|
||||
setColor: state.setIconColor,
|
||||
theme: state.applicationTheme,
|
||||
})));
|
||||
|
||||
const handleColorChange = useCallback(
|
||||
(event: React.ChangeEvent<HTMLInputElement>) => {
|
||||
@@ -46,9 +49,9 @@ const ColorInput = (_: ColorInputProps) => {
|
||||
onChange={throttledColorChange}
|
||||
value={color}
|
||||
/>
|
||||
<span style={{ color: isDark ? "black" : "white" }}>
|
||||
<span style={{ color: theme === ApplicationTheme.DARK ? "black" : "white" }}>
|
||||
{color === "currentColor" ? (
|
||||
<EyedropperSample size={28} weight="fill" />
|
||||
<EyedropperSampleIcon size={28} weight="fill" />
|
||||
) : (
|
||||
color
|
||||
)}
|
||||
|
||||
@@ -1,13 +1,12 @@
|
||||
import { useRecoilValue } from "recoil";
|
||||
import { motion, AnimatePresence, Variants } from "framer-motion";
|
||||
import { ArrowULeftUp, Coffee, HandHeart } from "@phosphor-icons/react";
|
||||
import { ArrowULeftUpIcon, CoffeeIcon, HandHeartIcon } from "@phosphor-icons/react";
|
||||
|
||||
import Links from "@/components/Links/Links";
|
||||
|
||||
import { ReactComponent as RulerMarker } from "@/assets/ruler-marker.svg";
|
||||
import { ReactComponent as RulerMarkerSpec } from "@/assets/ruler-marker-spec.svg";
|
||||
import { useMediaQuery } from "@/hooks";
|
||||
import { selectionEntryAtom } from "@/state";
|
||||
import { useApplicationStore } from "@/state";
|
||||
import "./Footer.css";
|
||||
|
||||
type FooterProps = {};
|
||||
@@ -20,7 +19,7 @@ const variants: Variants = {
|
||||
|
||||
const Footer = (_: FooterProps) => {
|
||||
const isMobile = useMediaQuery("(max-width: 719px)");
|
||||
const isViewing = !!useRecoilValue(selectionEntryAtom);
|
||||
const isViewing = !!useApplicationStore.use.selectionEntry();
|
||||
|
||||
return (
|
||||
<footer>
|
||||
@@ -42,7 +41,7 @@ const Footer = (_: FooterProps) => {
|
||||
?.scrollIntoView({ behavior: "smooth", block: "start" });
|
||||
}}
|
||||
>
|
||||
<ArrowULeftUp size="1em" />
|
||||
<ArrowULeftUpIcon size="1em" />
|
||||
</motion.button>
|
||||
)}
|
||||
</AnimatePresence>
|
||||
@@ -137,7 +136,7 @@ const Footer = (_: FooterProps) => {
|
||||
)
|
||||
}
|
||||
>
|
||||
<Coffee size={24} />
|
||||
<CoffeeIcon size={24} />
|
||||
Buy us a coffee
|
||||
</button>
|
||||
<button
|
||||
@@ -150,7 +149,7 @@ const Footer = (_: FooterProps) => {
|
||||
)
|
||||
}
|
||||
>
|
||||
<HandHeart size={24} />
|
||||
<HandHeartIcon size={24} />
|
||||
Become a patron
|
||||
</button>
|
||||
</div>
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
import {
|
||||
ArrowCircleUpRight,
|
||||
ArrowCircleDown,
|
||||
MegaphoneSimple,
|
||||
HandHeart,
|
||||
ArrowCircleUpRightIcon,
|
||||
ArrowCircleDownIcon,
|
||||
MegaphoneSimpleIcon,
|
||||
HandHeartIcon,
|
||||
} from "@phosphor-icons/react";
|
||||
|
||||
import Banner from "@/components/Banner";
|
||||
@@ -40,30 +40,31 @@ const Header = (_: HeaderProps) => {
|
||||
return (
|
||||
<header>
|
||||
<Banner.Container>
|
||||
<Banner id={"2.1.0"}>
|
||||
<Banner id={"rename-notice"}>
|
||||
<div className="message">
|
||||
<MegaphoneSimple size={32} mirrored />
|
||||
<MegaphoneSimpleIcon size={32} mirrored />
|
||||
<small>
|
||||
Phosphor v2.1 is out, adding 268 new icons and some general
|
||||
revisions. Check our{" "}
|
||||
<a href="https://github.com/phosphor-icons/homepage/releases">
|
||||
Some packages may be renaming icons in coming versions, and deprecating older names. Existing names will continue to work, but we recommend upgrading at your convenience. See{" "}
|
||||
<a href="https://github.com/phosphor-icons/react/releases/tag/v2.1.8">
|
||||
release notes
|
||||
</a>{" "}
|
||||
to see what's changed!
|
||||
for details.
|
||||
</small>
|
||||
</div>
|
||||
</Banner>
|
||||
<Banner id={"buymeacoffee2"}>
|
||||
<div className="message">
|
||||
<HandHeart size={32} mirrored />
|
||||
<HandHeartIcon size={32} mirrored />
|
||||
<small>
|
||||
We are now processing donations via{" "}
|
||||
<a href="https://www.buymeacoffee.com/phosphoricons">
|
||||
Buy Me a Coffee
|
||||
</a>{" "}
|
||||
and{" "}
|
||||
</a>{", "}
|
||||
<a href="https://ko-fi.com/phosphoricons">
|
||||
Ko-fi
|
||||
</a>{", and "}
|
||||
<a href="https://opencollective.com/phosphoricons">
|
||||
Open Collective
|
||||
</a>
|
||||
! Your one-time or recurring contribution does a lot to keep us
|
||||
going. Please show us some support if you can!
|
||||
@@ -87,11 +88,11 @@ const Header = (_: HeaderProps) => {
|
||||
</h2>
|
||||
<div className="button-container">
|
||||
<button className="main-button" onClick={handleGetStarted}>
|
||||
<ArrowCircleUpRight size={24} />
|
||||
<ArrowCircleUpRightIcon size={24} />
|
||||
Get started
|
||||
</button>
|
||||
<button className="main-button" onClick={handleScrollToIcons}>
|
||||
<ArrowCircleDown size={24} />
|
||||
<ArrowCircleDownIcon size={24} />
|
||||
Explore icons
|
||||
</button>
|
||||
</div>
|
||||
|
||||
@@ -1,16 +1,8 @@
|
||||
import { useRef, useEffect } from "react";
|
||||
import { useRecoilValue } from "recoil";
|
||||
import { motion, useAnimation } from "framer-motion";
|
||||
import { IconContext } from "@phosphor-icons/react";
|
||||
|
||||
import {
|
||||
iconWeightAtom,
|
||||
iconSizeAtom,
|
||||
iconColorAtom,
|
||||
filteredQueryResultsSelector,
|
||||
isDarkThemeSelector,
|
||||
searchQueryAtom,
|
||||
} from "@/state";
|
||||
import { ApplicationTheme, useApplicationStore } from "@/state";
|
||||
import Notice from "@/components/Notice";
|
||||
|
||||
import Panel from "./Panel";
|
||||
@@ -31,12 +23,14 @@ const defaultSearchTags = [
|
||||
type IconGridProps = {};
|
||||
|
||||
const IconGrid = (_: IconGridProps) => {
|
||||
const weight = useRecoilValue(iconWeightAtom);
|
||||
const size = useRecoilValue(iconSizeAtom);
|
||||
const color = useRecoilValue(iconColorAtom);
|
||||
const isDark = useRecoilValue(isDarkThemeSelector);
|
||||
const query = useRecoilValue(searchQueryAtom);
|
||||
const filteredQueryResults = useRecoilValue(filteredQueryResultsSelector);
|
||||
const {
|
||||
iconWeight: weight,
|
||||
iconSize: size,
|
||||
iconColor: color,
|
||||
applicationTheme,
|
||||
filteredQueryResults,
|
||||
searchQuery: query,
|
||||
} = useApplicationStore();
|
||||
|
||||
const originOffset = useRef({ top: 0, left: 0 });
|
||||
const controls = useAnimation();
|
||||
@@ -74,7 +68,7 @@ const IconGrid = (_: IconGridProps) => {
|
||||
<IconGridItem
|
||||
key={iconEntry.name}
|
||||
index={index}
|
||||
isDark={isDark}
|
||||
isDark={applicationTheme === ApplicationTheme.DARK}
|
||||
entry={iconEntry}
|
||||
originOffset={originOffset}
|
||||
/>
|
||||
|
||||
@@ -5,11 +5,11 @@ import {
|
||||
MutableRefObject,
|
||||
HTMLAttributes,
|
||||
} from "react";
|
||||
import { useRecoilState } from "recoil";
|
||||
import { motion } from "framer-motion";
|
||||
import { useShallow } from "zustand/react/shallow";
|
||||
|
||||
import { IconEntry } from "@/lib";
|
||||
import { selectionEntryAtom } from "@/state";
|
||||
import { useApplicationStore } from "@/state";
|
||||
|
||||
interface IconGridItemProps extends HTMLAttributes<HTMLDivElement> {
|
||||
index: number;
|
||||
@@ -33,7 +33,10 @@ const itemVariants = {
|
||||
const IconGridItem = (props: IconGridItemProps) => {
|
||||
const { index, originOffset, entry, style } = props;
|
||||
const { name, Icon } = entry;
|
||||
const [selection, setSelectionEntry] = useRecoilState(selectionEntryAtom);
|
||||
const { selection, setSelectionEntry } = useApplicationStore(useShallow((state) => ({
|
||||
selection: state.selectionEntry,
|
||||
setSelectionEntry: state.setSelectionEntry,
|
||||
})));
|
||||
const isOpen = selection?.name === name;
|
||||
const isNew = entry.tags.includes("*new*");
|
||||
const isUpdated = entry.tags.includes("*updated*");
|
||||
|
||||
@@ -5,18 +5,17 @@ import React, {
|
||||
useMemo,
|
||||
HTMLAttributes,
|
||||
} 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,
|
||||
ArrowFatLinesDown,
|
||||
XCircle,
|
||||
CaretDoubleLeft,
|
||||
CaretDoubleRight,
|
||||
CopyIcon,
|
||||
CheckCircleIcon,
|
||||
ArrowFatLinesDownIcon,
|
||||
XCircleIcon,
|
||||
CaretDoubleLeftIcon,
|
||||
CaretDoubleRightIcon,
|
||||
} from "@phosphor-icons/react";
|
||||
import { IconStyle } from "@phosphor-icons/core";
|
||||
import ReactGA from "react-ga4";
|
||||
@@ -24,13 +23,7 @@ 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 { useApplicationStore } from "@/state";
|
||||
import { getCodeSnippets, supportsWeight } from "@/utils";
|
||||
|
||||
import TagCloud from "./TagCloud";
|
||||
@@ -82,7 +75,7 @@ const ActionButton = (
|
||||
} & HTMLAttributes<HTMLButtonElement>
|
||||
) => {
|
||||
const { active, download, label, ...rest } = props;
|
||||
const Icon = download ? ArrowFatLinesDown : Copy;
|
||||
const Icon = download ? ArrowFatLinesDownIcon : CopyIcon;
|
||||
return (
|
||||
<button
|
||||
{...rest}
|
||||
@@ -91,7 +84,7 @@ const ActionButton = (
|
||||
tabIndex={0}
|
||||
>
|
||||
{active ? (
|
||||
<CheckCircle size={20} color="var(--olive)" weight="fill" />
|
||||
<CheckCircleIcon size={20} color="var(--olive)" weight="fill" />
|
||||
) : (
|
||||
<Icon size={20} color="currentColor" weight="fill" />
|
||||
)}
|
||||
@@ -101,12 +94,14 @@ const ActionButton = (
|
||||
};
|
||||
|
||||
const Panel = () => {
|
||||
const [entry, setSelectionEntry] = useRecoilState(selectionEntryAtom);
|
||||
const {
|
||||
iconWeight: weight,
|
||||
iconSize: size,
|
||||
iconColor: color,
|
||||
selectionEntry: entry,
|
||||
setSelectionEntry,
|
||||
} = useApplicationStore();
|
||||
|
||||
const weight = useRecoilValue(iconWeightAtom);
|
||||
const size = useRecoilValue(iconSizeAtom);
|
||||
const color = useRecoilValue(iconColorAtom);
|
||||
const isDark = useRecoilValue(isDarkThemeSelector);
|
||||
const [copied, setCopied] = useTransientState<SnippetType | CopyType | false>(
|
||||
false,
|
||||
2000
|
||||
@@ -169,13 +164,13 @@ const Panel = () => {
|
||||
onClick={(e) => handleCopySnippet(e, type)}
|
||||
>
|
||||
{copied === type ? (
|
||||
<CheckCircle
|
||||
<CheckCircleIcon
|
||||
size={20}
|
||||
color="var(--olive)"
|
||||
weight="fill"
|
||||
/>
|
||||
) : (
|
||||
<Copy size={20} color="var(--foreground)" weight="fill" />
|
||||
<CopyIcon size={20} color="var(--foreground)" weight="fill" />
|
||||
)}
|
||||
</button>
|
||||
)}
|
||||
@@ -187,7 +182,7 @@ const Panel = () => {
|
||||
);
|
||||
|
||||
return [snippets, tabs];
|
||||
}, [entry, weight, size, color, copied, isDark]);
|
||||
}, [entry, weight, size, color, copied]);
|
||||
|
||||
useHotkeys("esc", () => setSelectionEntry(null));
|
||||
|
||||
@@ -232,11 +227,11 @@ const Panel = () => {
|
||||
|
||||
navigator.clipboard?.writeText(
|
||||
"data:image/svg+xml;base64," +
|
||||
btoa(
|
||||
unescape(
|
||||
encodeURIComponent(cloneWithSize(ref.current, size).outerHTML)
|
||||
)
|
||||
btoa(
|
||||
unescape(
|
||||
encodeURIComponent(cloneWithSize(ref.current, size).outerHTML)
|
||||
)
|
||||
)
|
||||
);
|
||||
setCopied(CopyType.SVG_DATA);
|
||||
};
|
||||
@@ -246,8 +241,7 @@ const Panel = () => {
|
||||
|
||||
const { name } = entry;
|
||||
const data = await fetch(
|
||||
`https://raw.githubusercontent.com/phosphor-icons/core/main/raw/${weight}/${name}${
|
||||
weight === "regular" ? "" : `-${weight}`
|
||||
`https://raw.githubusercontent.com/phosphor-icons/core/main/raw/${weight}/${name}${weight === "regular" ? "" : `-${weight}`
|
||||
}.svg`
|
||||
);
|
||||
const content = await data.text();
|
||||
@@ -282,8 +276,7 @@ const Panel = () => {
|
||||
|
||||
const { name } = entry;
|
||||
saveAs(
|
||||
`https://raw.githubusercontent.com/phosphor-icons/core/main/raw/${weight}/${name}${
|
||||
weight === "regular" ? "" : `-${weight}`
|
||||
`https://raw.githubusercontent.com/phosphor-icons/core/main/raw/${weight}/${name}${weight === "regular" ? "" : `-${weight}`
|
||||
}.svg`,
|
||||
`${entry?.name}${weight === "regular" ? "" : `-${weight}`}.svg`
|
||||
);
|
||||
@@ -433,13 +426,13 @@ const Panel = () => {
|
||||
onClick={() => setShowMoreActions((s) => !s)}
|
||||
>
|
||||
{!showMoreActions ? (
|
||||
<CaretDoubleRight
|
||||
<CaretDoubleRightIcon
|
||||
size={16}
|
||||
weight="bold"
|
||||
color="var(--foreground)"
|
||||
/>
|
||||
) : (
|
||||
<CaretDoubleLeft
|
||||
<CaretDoubleLeftIcon
|
||||
size={16}
|
||||
weight="bold"
|
||||
color="var(--foreground)"
|
||||
@@ -459,7 +452,7 @@ const Panel = () => {
|
||||
e.key === "Enter" && setSelectionEntry(null);
|
||||
}}
|
||||
>
|
||||
<XCircle color="currentColor" size={28} weight="fill" />
|
||||
<XCircleIcon color="currentColor" size={28} weight="fill" />
|
||||
</button>
|
||||
</motion.aside>
|
||||
)}
|
||||
|
||||
@@ -1,8 +1,7 @@
|
||||
import { useCallback } from "react";
|
||||
import { useSetRecoilState } from "recoil";
|
||||
|
||||
import { useMediaQuery } from "@/hooks";
|
||||
import { searchQueryAtom } from "@/state";
|
||||
import { useApplicationStore } from "@/state";
|
||||
import "./TagCloud.css";
|
||||
|
||||
interface TagCloudProps {
|
||||
@@ -12,7 +11,7 @@ interface TagCloudProps {
|
||||
|
||||
const TagCloud = ({ name, tags }: TagCloudProps) => {
|
||||
const isMobile = useMediaQuery("(max-width: 719px)");
|
||||
const setQuery = useSetRecoilState(searchQueryAtom);
|
||||
const setQuery = useApplicationStore.use.setSearchQuery();
|
||||
const handleTagClick = useCallback(
|
||||
(tag: string) => {
|
||||
setQuery(tag);
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { ArrowElbowDownRight } from "@phosphor-icons/react";
|
||||
import { ArrowElbowDownRightIcon } from "@phosphor-icons/react";
|
||||
|
||||
import { iconCount } from "@/lib/icons";
|
||||
import OutboundLink from "@/components/OutboundLink";
|
||||
@@ -11,7 +11,7 @@ const Links = (_: LinksProps) => {
|
||||
return (
|
||||
<div className="links">
|
||||
<div>
|
||||
<ArrowElbowDownRight size={24} />
|
||||
<ArrowElbowDownRightIcon size={24} />
|
||||
<OutboundLink
|
||||
className="nav-link"
|
||||
href="/assets/phosphor-icons.zip"
|
||||
@@ -24,7 +24,7 @@ const Links = (_: LinksProps) => {
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<ArrowElbowDownRight size={24} />
|
||||
<ArrowElbowDownRightIcon size={24} />
|
||||
<span>
|
||||
<OutboundLink
|
||||
href="https://www.figma.com/community/plugin/898620911119764089/Phosphor-Icons"
|
||||
@@ -43,7 +43,7 @@ const Links = (_: LinksProps) => {
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<ArrowElbowDownRight size={24} />
|
||||
<ArrowElbowDownRightIcon size={24} />
|
||||
<OutboundLink
|
||||
href="/assets/phosphor-icons.sketchplugin.zip"
|
||||
eventLabel="Download sketch plugin"
|
||||
@@ -55,7 +55,7 @@ const Links = (_: LinksProps) => {
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<ArrowElbowDownRight size={24} />
|
||||
<ArrowElbowDownRightIcon size={24} />
|
||||
<OutboundLink
|
||||
href="https://play.phosphoricons.com"
|
||||
eventLabel="Showcase"
|
||||
@@ -65,7 +65,7 @@ const Links = (_: LinksProps) => {
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<ArrowElbowDownRight size={24} />
|
||||
<ArrowElbowDownRightIcon size={24} />
|
||||
<OutboundLink
|
||||
href="https://github.com/phosphor-icons/homepage"
|
||||
eventLabel="GitHub"
|
||||
@@ -74,7 +74,7 @@ const Links = (_: LinksProps) => {
|
||||
</OutboundLink>
|
||||
</div>
|
||||
<div>
|
||||
<ArrowElbowDownRight size={24} />
|
||||
<ArrowElbowDownRightIcon size={24} />
|
||||
<OutboundLink
|
||||
href="https://github.com/phosphor-icons/homepage/issues"
|
||||
eventLabel="Request"
|
||||
@@ -84,7 +84,7 @@ const Links = (_: LinksProps) => {
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<ArrowElbowDownRight size={24} />
|
||||
<ArrowElbowDownRightIcon size={24} />
|
||||
<span>
|
||||
<OutboundLink
|
||||
href="https://www.buymeacoffee.com/phosphoricons"
|
||||
@@ -103,7 +103,7 @@ const Links = (_: LinksProps) => {
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<ArrowElbowDownRight size={24} />
|
||||
<ArrowElbowDownRightIcon size={24} />
|
||||
<OutboundLink
|
||||
href="https://twitter.com/_phosphoricons"
|
||||
eventLabel="Twitter"
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { ReactNode } from "react";
|
||||
import { motion } from "framer-motion";
|
||||
import { HourglassMedium, Question, SmileyXEyes } from "@phosphor-icons/react";
|
||||
import { HourglassMediumIcon, QuestionIcon, SmileyXEyesIcon } from "@phosphor-icons/react";
|
||||
|
||||
interface NoticeProps {
|
||||
message?: ReactNode;
|
||||
@@ -22,9 +22,9 @@ const Notice = ({
|
||||
transition={{ duration: 0.5 }}
|
||||
>
|
||||
<div className="empty-list-box">
|
||||
{type === "wait" && <HourglassMedium size={128} weight="fill" />}
|
||||
{type === "help" && <Question size={128} weight="fill" />}
|
||||
{type === "warn" && <SmileyXEyes size={128} weight="fill" />}
|
||||
{type === "wait" && <HourglassMediumIcon size={128} weight="fill" />}
|
||||
{type === "help" && <QuestionIcon size={128} weight="fill" />}
|
||||
{type === "warn" && <SmileyXEyesIcon size={128} weight="fill" />}
|
||||
<p>{message}</p>
|
||||
{children}
|
||||
</div>
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { ArrowCircleUpRight } from "@phosphor-icons/react";
|
||||
import { ArrowCircleUpRightIcon } from "@phosphor-icons/react";
|
||||
|
||||
export type RecipeProps = {
|
||||
title: string;
|
||||
@@ -12,7 +12,7 @@ const Recipe = ({ url, Example }: RecipeProps) => {
|
||||
{/* <h1>{title}</h1> */}
|
||||
<div className="recipe-linkout">
|
||||
<span>Open on StackBlitz</span>
|
||||
<ArrowCircleUpRight weight="fill" size={32} />
|
||||
<ArrowCircleUpRightIcon weight="fill" size={32} />
|
||||
</div>
|
||||
<Example />
|
||||
</a>
|
||||
|
||||
@@ -2,10 +2,10 @@ import { useMemo } from "react";
|
||||
import {
|
||||
Icon,
|
||||
IconProps,
|
||||
Barricade,
|
||||
GasCan,
|
||||
IceCream,
|
||||
FlyingSaucer,
|
||||
BarricadeIcon,
|
||||
GasCanIcon,
|
||||
IceCreamIcon,
|
||||
FlyingSaucerIcon,
|
||||
} from "@phosphor-icons/react";
|
||||
|
||||
import { RecipeProps } from "../Recipe";
|
||||
@@ -51,10 +51,10 @@ const duocolor: RecipeProps = {
|
||||
Example() {
|
||||
return (
|
||||
<div className="example">
|
||||
<Duocolor Icon={FlyingSaucer} duocolor="darkcyan" />
|
||||
<Duocolor Icon={Barricade} color="darkgray" duocolor="orange" />
|
||||
<Duocolor Icon={IceCream} color="saddlebrown" duocolor="lightpink" />
|
||||
<Duocolor Icon={GasCan} duocolor="indianred" />
|
||||
<Duocolor Icon={FlyingSaucerIcon} duocolor="darkcyan" />
|
||||
<Duocolor Icon={BarricadeIcon} color="darkgray" duocolor="orange" />
|
||||
<Duocolor Icon={IceCreamIcon} color="saddlebrown" duocolor="lightpink" />
|
||||
<Duocolor Icon={GasCanIcon} duocolor="indianred" />
|
||||
</div>
|
||||
);
|
||||
},
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { Fire, Image, Peace, RainbowCloud } from "@phosphor-icons/react";
|
||||
import { FireIcon, ImageIcon, PeaceIcon, RainbowCloudIcon } from "@phosphor-icons/react";
|
||||
|
||||
import { RecipeProps } from "../Recipe";
|
||||
|
||||
@@ -8,7 +8,7 @@ const gradient: RecipeProps = {
|
||||
Example() {
|
||||
return (
|
||||
<div className="example">
|
||||
<Fire weight="fill" color="url(#flame)">
|
||||
<FireIcon weight="fill" color="url(#flame)">
|
||||
<defs>
|
||||
<linearGradient id="flame" x1="0%" y1="100%" x2="0%" y2="0%">
|
||||
<stop offset="10%" stopColor="#FFDB00" />
|
||||
@@ -18,9 +18,9 @@ const gradient: RecipeProps = {
|
||||
<stop offset="95%" stopColor="#E25822" />
|
||||
</linearGradient>
|
||||
</defs>
|
||||
</Fire>
|
||||
</FireIcon>
|
||||
|
||||
<RainbowCloud color="url(#spectrum)">
|
||||
<RainbowCloudIcon color="url(#spectrum)">
|
||||
<defs>
|
||||
<linearGradient id="spectrum">
|
||||
<stop offset="10%" stopColor="indigo" />
|
||||
@@ -30,9 +30,9 @@ const gradient: RecipeProps = {
|
||||
<stop offset="95%" stopColor="red" />
|
||||
</linearGradient>
|
||||
</defs>
|
||||
</RainbowCloud>
|
||||
</RainbowCloudIcon>
|
||||
|
||||
<Peace weight="fill" color="url(#spectrum2)">
|
||||
<PeaceIcon weight="fill" color="url(#spectrum2)">
|
||||
<defs>
|
||||
<radialGradient id="spectrum2">
|
||||
<stop offset="15%" stopColor="indigo" />
|
||||
@@ -42,16 +42,16 @@ const gradient: RecipeProps = {
|
||||
<stop offset="95%" stopColor="red" />
|
||||
</radialGradient>
|
||||
</defs>
|
||||
</Peace>
|
||||
</PeaceIcon>
|
||||
|
||||
<Image color="url(#sunset)" weight="fill">
|
||||
<ImageIcon color="url(#sunset)" weight="fill">
|
||||
<defs>
|
||||
<linearGradient id="sunset" x1="0%" y1="100%" x2="100%" y2="0%">
|
||||
<stop offset="0%" stopColor="violet" />
|
||||
<stop offset="100%" stopColor="yellow" />
|
||||
</linearGradient>
|
||||
</defs>
|
||||
</Image>
|
||||
</ImageIcon>
|
||||
</div>
|
||||
);
|
||||
},
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { CassetteTape, Cube, Virus, ThumbsUp } from "@phosphor-icons/react";
|
||||
import { CassetteTapeIcon, CubeIcon, VirusIcon, ThumbsUpIcon } from "@phosphor-icons/react";
|
||||
|
||||
import { RecipeProps } from "../Recipe";
|
||||
|
||||
@@ -8,7 +8,7 @@ const animation: RecipeProps = {
|
||||
Example() {
|
||||
return (
|
||||
<div className="example">
|
||||
<CassetteTape
|
||||
<CassetteTapeIcon
|
||||
color="teal"
|
||||
style={{ filter: "url(#displacementFilter)" }}
|
||||
>
|
||||
@@ -29,10 +29,10 @@ const animation: RecipeProps = {
|
||||
/>
|
||||
</filter>
|
||||
</defs>
|
||||
</CassetteTape>
|
||||
<Cube color="teal" style={{ filter: "url(#displacementFilter)" }} />
|
||||
<ThumbsUp color="teal" style={{ filter: "url(#displacementFilter)" }} />
|
||||
<Virus color="teal" style={{ filter: "url(#displacementFilter)" }} />
|
||||
</CassetteTapeIcon>
|
||||
<CubeIcon color="teal" style={{ filter: "url(#displacementFilter)" }} />
|
||||
<ThumbsUpIcon color="teal" style={{ filter: "url(#displacementFilter)" }} />
|
||||
<VirusIcon color="teal" style={{ filter: "url(#displacementFilter)" }} />
|
||||
</div>
|
||||
);
|
||||
},
|
||||
|
||||
@@ -5,19 +5,19 @@ import {
|
||||
MutableRefObject,
|
||||
ReactNode,
|
||||
} from "react";
|
||||
import { useRecoilState } from "recoil";
|
||||
import { useShallow } from "zustand/react/shallow";
|
||||
import { useHotkeys } from "react-hotkeys-hook";
|
||||
import {
|
||||
Command,
|
||||
MagnifyingGlass,
|
||||
X,
|
||||
HourglassHigh,
|
||||
CommandIcon,
|
||||
MagnifyingGlassIcon,
|
||||
XIcon,
|
||||
HourglassHighIcon,
|
||||
} from "@phosphor-icons/react";
|
||||
import ReactGA from "react-ga4";
|
||||
|
||||
import { useDebounce } from "@/hooks";
|
||||
import { searchQueryAtom } from "@/state";
|
||||
import "./SearchInput.css";
|
||||
import { useApplicationStore } from "@/state";
|
||||
|
||||
const apple = /iPhone|iPod|iPad|Macintosh|MacIntel|MacPPC/i;
|
||||
const isApple = apple.test(window.navigator.platform);
|
||||
@@ -29,7 +29,10 @@ type SearchInputProps = {};
|
||||
|
||||
const SearchInput = (_: SearchInputProps) => {
|
||||
const [value, setValue] = useState<string>("");
|
||||
const [query, setQuery] = useRecoilState(searchQueryAtom);
|
||||
const { query, setQuery } = useApplicationStore(useShallow((state) => ({
|
||||
query: state.searchQuery,
|
||||
setQuery: state.setSearchQuery,
|
||||
})));
|
||||
const inputRef =
|
||||
useRef<HTMLInputElement>() as MutableRefObject<HTMLInputElement>;
|
||||
|
||||
@@ -77,7 +80,7 @@ const SearchInput = (_: SearchInputProps) => {
|
||||
|
||||
return (
|
||||
<div className="search-bar">
|
||||
<MagnifyingGlass id="search-icon" size={24} />
|
||||
<MagnifyingGlassIcon id="search-icon" size={24} />
|
||||
<input
|
||||
ref={inputRef}
|
||||
id="search-input"
|
||||
@@ -93,12 +96,12 @@ const SearchInput = (_: SearchInputProps) => {
|
||||
(key === "Enter" || key === "Escape") && currentTarget.blur()
|
||||
}
|
||||
/>
|
||||
{!value && !isMobile && <Keys>{isApple ? <Command /> : "Ctrl + "}K</Keys>}
|
||||
{!value && !isMobile && <Keys>{isApple ? <CommandIcon /> : "Ctrl + "}K</Keys>}
|
||||
{value ? (
|
||||
value === query ? (
|
||||
<X className="clear-icon" size={18} onClick={handleCancelSearch} />
|
||||
<XIcon className="clear-icon" size={18} onClick={handleCancelSearch} />
|
||||
) : (
|
||||
<HourglassHigh className="wait-icon" weight="fill" size={18} />
|
||||
<HourglassHighIcon className="wait-icon" weight="fill" size={18} />
|
||||
)
|
||||
) : null}
|
||||
</div>
|
||||
|
||||
@@ -1,29 +1,27 @@
|
||||
import { useState } from "react";
|
||||
import ReactGA from "react-ga4";
|
||||
import { useRecoilState, useResetRecoilState, useSetRecoilState } from "recoil";
|
||||
import { useShallow } from "zustand/react/shallow";
|
||||
import {
|
||||
ArrowCounterClockwise,
|
||||
CheckCircle,
|
||||
DiceFive,
|
||||
Link,
|
||||
ArrowCounterClockwiseIcon,
|
||||
CheckCircleIcon,
|
||||
DiceFiveIcon,
|
||||
LinkIcon,
|
||||
} from "@phosphor-icons/react";
|
||||
import { IconStyle } from "@phosphor-icons/core";
|
||||
|
||||
import { useTransientState } from "@/hooks";
|
||||
import {
|
||||
iconWeightAtom,
|
||||
iconSizeAtom,
|
||||
iconColorAtom,
|
||||
resetSettingsSelector,
|
||||
} from "@/state";
|
||||
import { useApplicationStore } from "@/state";
|
||||
|
||||
import "./SettingsActions.css";
|
||||
|
||||
const SettingsActions = () => {
|
||||
const [weight, setWeight] = useRecoilState(iconWeightAtom);
|
||||
const setSize = useSetRecoilState(iconSizeAtom);
|
||||
const setColor = useSetRecoilState(iconColorAtom);
|
||||
const reset = useResetRecoilState(resetSettingsSelector);
|
||||
const { weight, setWeight, setSize, setColor, reset } = useApplicationStore(useShallow((state) => ({
|
||||
weight: state.iconWeight,
|
||||
setWeight: state.setIconWeight,
|
||||
setSize: state.setIconSize,
|
||||
setColor: state.setIconColor,
|
||||
reset: state.resetApplicationState,
|
||||
})));
|
||||
|
||||
const [copied, setCopied] = useTransientState<boolean>(false, 2000);
|
||||
const [booped, setBooped] = useState<boolean>(false);
|
||||
@@ -67,7 +65,7 @@ const SettingsActions = () => {
|
||||
title="Restore default settings"
|
||||
onClick={reset}
|
||||
>
|
||||
<ArrowCounterClockwise size={24} />
|
||||
<ArrowCounterClockwiseIcon size={24} />
|
||||
</button>
|
||||
<button
|
||||
className="tool-button"
|
||||
@@ -75,9 +73,9 @@ const SettingsActions = () => {
|
||||
onClick={copyDeepLinkToClipboard}
|
||||
>
|
||||
{copied ? (
|
||||
<CheckCircle size={24} color="var(--olive)" weight="fill" />
|
||||
<CheckCircleIcon size={24} color="var(--olive)" weight="fill" />
|
||||
) : (
|
||||
<Link size={24} />
|
||||
<LinkIcon size={24} />
|
||||
)}
|
||||
</button>
|
||||
<button
|
||||
@@ -90,7 +88,7 @@ const SettingsActions = () => {
|
||||
style={{ display: "flex" }}
|
||||
onAnimationEnd={() => setBooped(false)}
|
||||
>
|
||||
<DiceFive className={booped ? "spin" : ""} size={24} />
|
||||
<DiceFiveIcon className={booped ? "spin" : ""} size={24} />
|
||||
</span>
|
||||
</button>
|
||||
</>
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import React, { useCallback } from "react";
|
||||
import { useRecoilState } from "recoil";
|
||||
import { useShallow } from "zustand/react/shallow";
|
||||
|
||||
import { iconSizeAtom } from "@/state";
|
||||
import { useApplicationStore } from "@/state";
|
||||
import "./SizeInput.css";
|
||||
|
||||
type SizeInputProps = {};
|
||||
@@ -15,7 +15,10 @@ const handleBlur = (event: React.UIEvent<HTMLInputElement>) => {
|
||||
};
|
||||
|
||||
const SizeInput = (_: SizeInputProps) => {
|
||||
const [size, setSize] = useRecoilState(iconSizeAtom);
|
||||
const { size, setSize } = useApplicationStore(useShallow((state) => ({
|
||||
size: state.iconSize,
|
||||
setSize: state.setIconSize,
|
||||
})));
|
||||
|
||||
const handleSizeChange = useCallback(
|
||||
(event: React.ChangeEvent<HTMLInputElement>) => {
|
||||
|
||||
@@ -1,10 +1,9 @@
|
||||
import { useMemo } from "react";
|
||||
import { useRecoilState } from "recoil";
|
||||
import { useShallow } from "zustand/react/shallow";
|
||||
import Select from "react-dropdown-select";
|
||||
import { PencilSimpleLine } from "@phosphor-icons/react";
|
||||
import { PencilSimpleLineIcon } from "@phosphor-icons/react";
|
||||
import { IconStyle } from "@phosphor-icons/core";
|
||||
|
||||
import { iconWeightAtom } from "@/state";
|
||||
import { useApplicationStore } from "@/state";
|
||||
|
||||
import "./StyleInput.css";
|
||||
|
||||
@@ -14,44 +13,45 @@ const options: WeightOption[] = [
|
||||
{
|
||||
key: "Thin",
|
||||
value: IconStyle.THIN,
|
||||
icon: <PencilSimpleLine size={24} weight="thin" />,
|
||||
icon: <PencilSimpleLineIcon size={24} weight="thin" />,
|
||||
},
|
||||
{
|
||||
key: "Light",
|
||||
value: IconStyle.LIGHT,
|
||||
icon: <PencilSimpleLine size={24} weight="light" />,
|
||||
icon: <PencilSimpleLineIcon size={24} weight="light" />,
|
||||
},
|
||||
{
|
||||
key: "Regular",
|
||||
value: IconStyle.REGULAR,
|
||||
icon: <PencilSimpleLine size={24} weight="regular" />,
|
||||
icon: <PencilSimpleLineIcon size={24} weight="regular" />,
|
||||
},
|
||||
{
|
||||
key: "Bold",
|
||||
value: IconStyle.BOLD,
|
||||
icon: <PencilSimpleLine size={24} weight="bold" />,
|
||||
icon: <PencilSimpleLineIcon size={24} weight="bold" />,
|
||||
},
|
||||
{
|
||||
key: "Fill",
|
||||
value: IconStyle.FILL,
|
||||
icon: <PencilSimpleLine size={24} weight="fill" />,
|
||||
icon: <PencilSimpleLineIcon size={24} weight="fill" />,
|
||||
},
|
||||
{
|
||||
key: "Duotone",
|
||||
value: IconStyle.DUOTONE,
|
||||
icon: <PencilSimpleLine size={24} weight="duotone" />,
|
||||
icon: <PencilSimpleLineIcon size={24} weight="duotone" />,
|
||||
},
|
||||
];
|
||||
|
||||
type StyleInputProps = {};
|
||||
|
||||
const StyleInput = (_: StyleInputProps) => {
|
||||
const [style, setStyle] = useRecoilState(iconWeightAtom);
|
||||
|
||||
const currentStyle = useMemo(
|
||||
() => [options.find((option) => option.value === style)!!],
|
||||
[style]
|
||||
);
|
||||
const StyleInput = (_: StyleInputProps) => {
|
||||
const { style, setStyle } = useApplicationStore(useShallow((state) => ({
|
||||
style: state.iconWeight,
|
||||
setStyle: state.setIconWeight,
|
||||
})));
|
||||
|
||||
const currentStyle = [options.find((option) => option.value === style)!];
|
||||
|
||||
const handleStyleChange = (values: WeightOption[]) =>
|
||||
setStyle(values[0].value as IconStyle);
|
||||
@@ -72,9 +72,8 @@ const StyleInput = (_: StyleInputProps) => {
|
||||
<span
|
||||
role="option"
|
||||
aria-selected={item.key === values[0].key}
|
||||
className={`react-dropdown-select-item ${
|
||||
itemIndex === cursor ? "react-dropdown-select-item-active" : ""
|
||||
}`}
|
||||
className={`react-dropdown-select-item ${itemIndex === cursor ? "react-dropdown-select-item-active" : ""
|
||||
}`}
|
||||
onClick={() => methods.addItem(item)}
|
||||
>
|
||||
{item.icon}
|
||||
|
||||
@@ -3,7 +3,7 @@ export { default as useDebounce } from "./useDebounce";
|
||||
export { default as useEvent } from "./useEvent";
|
||||
export { default as useLocalStorage } from "./useLocalStorage";
|
||||
export { default as useMediaQuery } from "./useMediaQuery";
|
||||
export { default as usePersistSettings } from "./usePersistSettings";
|
||||
// export { default as usePersistSettings } from "./usePersistSettings";
|
||||
export { default as useSessionStorage } from "./useSessionStorage";
|
||||
export { default as useThrottle } from "./useThrottle";
|
||||
export { default as useThrottled } from "./useThrottled";
|
||||
|
||||
@@ -1,23 +0,0 @@
|
||||
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]
|
||||
);
|
||||
}
|
||||
@@ -1,7 +1,5 @@
|
||||
import { StrictMode } from "react";
|
||||
import { createRoot } from "react-dom/client";
|
||||
import { RecoilRoot } from "recoil";
|
||||
import { RecoilURLSyncJSON } from "recoil-sync";
|
||||
import App from "./components/App";
|
||||
import ReactGA from "react-ga4";
|
||||
import ErrorBoundary from "./components/ErrorBoundary";
|
||||
@@ -15,24 +13,20 @@ const root = createRoot(container!);
|
||||
|
||||
root.render(
|
||||
<StrictMode>
|
||||
<RecoilRoot>
|
||||
<ErrorBoundary
|
||||
fallback={
|
||||
<Notice
|
||||
message={
|
||||
<p>
|
||||
An error occurred. Try going{" "}
|
||||
<a href={window.location.origin}>home</a>.
|
||||
</p>
|
||||
}
|
||||
/>
|
||||
}
|
||||
>
|
||||
<RecoilURLSyncJSON location={{ part: "queryParams" }}>
|
||||
<App />
|
||||
</RecoilURLSyncJSON>
|
||||
</ErrorBoundary>
|
||||
</RecoilRoot>
|
||||
<ErrorBoundary
|
||||
fallback={
|
||||
<Notice
|
||||
message={
|
||||
<p>
|
||||
An error occurred. Try going{" "}
|
||||
<a href={window.location.origin}>home</a>.
|
||||
</p>
|
||||
}
|
||||
/>
|
||||
}
|
||||
>
|
||||
<App />
|
||||
</ErrorBoundary>
|
||||
</StrictMode>
|
||||
);
|
||||
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { Icon } from "@phosphor-icons/react";
|
||||
import { IconEntry as CoreEntry } from "@phosphor-icons/core";
|
||||
export * from "./icons";
|
||||
|
||||
export interface IconEntry extends CoreEntry {
|
||||
Icon: Icon;
|
||||
|
||||
@@ -1,85 +0,0 @@
|
||||
import { useCallback, ReactNode } from "react";
|
||||
import { DefaultValue } from "recoil";
|
||||
import { ReadItem, WriteItems, ListenToItems, RecoilSync } from "recoil-sync";
|
||||
import { STORAGE_KEY } from ".";
|
||||
|
||||
const DEFAULT_VALUE = new DefaultValue();
|
||||
|
||||
export default ({ children }: { children: ReactNode }) => {
|
||||
const read: ReadItem = useCallback((itemKey) => {
|
||||
if (typeof document === "undefined") return DEFAULT_VALUE; // SSR
|
||||
|
||||
const item = localStorage.getItem(itemKey);
|
||||
|
||||
let parsed: unknown;
|
||||
|
||||
try {
|
||||
parsed = item === null ? DEFAULT_VALUE : parseJSON(item);
|
||||
} catch {
|
||||
parsed = DEFAULT_VALUE;
|
||||
console.warn({ itemKey, item }, "parseJSON failed");
|
||||
}
|
||||
|
||||
return parsed;
|
||||
}, []);
|
||||
|
||||
const write: WriteItems = useCallback(({ diff }) => {
|
||||
if (typeof document === "undefined") return; // SSR
|
||||
|
||||
for (const [key, value] of diff) {
|
||||
if (value instanceof DefaultValue) {
|
||||
localStorage.removeItem(key);
|
||||
} else {
|
||||
// reasons for setItem to fail: https://developer.mozilla.org/en-US/docs/Web/API/Storage/setItem#exceptions
|
||||
try {
|
||||
localStorage.setItem(key, JSON.stringify(value));
|
||||
} catch (err) {
|
||||
console.warn({ err, key, value }, "localStorage.setItem failed");
|
||||
}
|
||||
}
|
||||
}
|
||||
}, []);
|
||||
|
||||
const listen: ListenToItems = useCallback(
|
||||
({ updateItem, updateAllKnownItems }) => {
|
||||
void updateAllKnownItems;
|
||||
const onStorage = (event: StorageEvent) => {
|
||||
// ignore clear() calls
|
||||
if (event.storageArea === localStorage && event.key !== null) {
|
||||
let parsed: unknown;
|
||||
try {
|
||||
parsed =
|
||||
event.newValue === null
|
||||
? DEFAULT_VALUE
|
||||
: parseJSON(event.newValue);
|
||||
} catch {
|
||||
parsed = DEFAULT_VALUE;
|
||||
console.warn({ event }, "parseJSON failed");
|
||||
}
|
||||
|
||||
updateItem(event.key, parsed);
|
||||
}
|
||||
};
|
||||
|
||||
window.addEventListener("storage", onStorage);
|
||||
|
||||
return () => window.removeEventListener("storage", onStorage);
|
||||
},
|
||||
[]
|
||||
);
|
||||
|
||||
return (
|
||||
<RecoilSync
|
||||
storeKey={STORAGE_KEY}
|
||||
read={read}
|
||||
write={write}
|
||||
listen={listen}
|
||||
>
|
||||
{children}
|
||||
</RecoilSync>
|
||||
);
|
||||
};
|
||||
|
||||
function parseJSON(value: string): unknown {
|
||||
return value === "undefined" ? undefined : JSON.parse(value);
|
||||
}
|
||||
@@ -1,99 +0,0 @@
|
||||
import { atom } from "recoil";
|
||||
import { syncEffect } from "recoil-sync";
|
||||
import TinyColor from "tinycolor2";
|
||||
import { custom, stringLiterals } from "@recoiljs/refine";
|
||||
import { IconStyle } from "@phosphor-icons/core";
|
||||
import { IconEntry } from "@/lib";
|
||||
|
||||
export const searchQueryAtom = atom<string>({
|
||||
key: "searchQuery",
|
||||
default: "",
|
||||
effects: [
|
||||
syncEffect({
|
||||
itemKey: "q",
|
||||
refine: custom((q) => {
|
||||
return (q as string).toString() ?? "";
|
||||
}),
|
||||
syncDefault: false,
|
||||
}),
|
||||
],
|
||||
});
|
||||
|
||||
export const iconWeightAtom = atom<IconStyle>({
|
||||
key: "iconWeight",
|
||||
default: IconStyle.REGULAR,
|
||||
effects: [
|
||||
syncEffect<IconStyle>({
|
||||
itemKey: "weight",
|
||||
refine: stringLiterals({
|
||||
thin: IconStyle.THIN,
|
||||
light: IconStyle.LIGHT,
|
||||
regular: IconStyle.REGULAR,
|
||||
bold: IconStyle.BOLD,
|
||||
fill: IconStyle.FILL,
|
||||
duotone: IconStyle.DUOTONE,
|
||||
}),
|
||||
write: (atom, w) => {
|
||||
if (typeof w === "string") {
|
||||
atom.write("weight", w);
|
||||
} else {
|
||||
atom.reset("weight");
|
||||
}
|
||||
},
|
||||
syncDefault: false,
|
||||
}),
|
||||
],
|
||||
});
|
||||
|
||||
export const iconSizeAtom = atom<number>({
|
||||
key: "iconSize",
|
||||
default: 32,
|
||||
effects: [
|
||||
syncEffect({
|
||||
itemKey: "size",
|
||||
refine: custom((s) => {
|
||||
const size = Number.isFinite(Number(s)) ? Number(s) : 32;
|
||||
return Math.min(Math.max(size, 16), 96);
|
||||
}),
|
||||
syncDefault: false,
|
||||
}),
|
||||
],
|
||||
});
|
||||
|
||||
export const iconColorAtom = atom<string>({
|
||||
key: "iconColor",
|
||||
default: "#000000",
|
||||
effects: [
|
||||
syncEffect({
|
||||
itemKey: "color",
|
||||
refine: custom((c) => {
|
||||
if (typeof c === "string") {
|
||||
const normalizedColor = TinyColor(c);
|
||||
if (normalizedColor.isValid()) {
|
||||
return normalizedColor.toHexString();
|
||||
}
|
||||
}
|
||||
return "#000000";
|
||||
}),
|
||||
write: (atom, c) => {
|
||||
if (typeof c === "string") {
|
||||
const color = c.replace("#", "");
|
||||
atom.write("color", color);
|
||||
} else {
|
||||
atom.reset("color");
|
||||
}
|
||||
},
|
||||
syncDefault: false,
|
||||
}),
|
||||
],
|
||||
});
|
||||
|
||||
export const iconPreviewOpenAtom = atom<string | false>({
|
||||
key: "iconPreviewOpen",
|
||||
default: false,
|
||||
});
|
||||
|
||||
export const selectionEntryAtom = atom<IconEntry | null>({
|
||||
key: "selectionEntry",
|
||||
default: null,
|
||||
});
|
||||
@@ -1,4 +1,238 @@
|
||||
export * from "./atoms";
|
||||
export * from "./selectors";
|
||||
import Fuse from "fuse.js";
|
||||
import { create, type UseBoundStore, type StoreApi } from "zustand";
|
||||
import { persist, PersistStorage } from "zustand/middleware";
|
||||
|
||||
import TinyColor from "tinycolor2";
|
||||
import { IconStyle } from "@phosphor-icons/core";
|
||||
import { type IconEntry, icons } from "@/lib";
|
||||
import { parseColor, parseQuery, parseSize, parseWeight } from "@/utils";
|
||||
|
||||
export const STORAGE_KEY = "__phosphor_settings__";
|
||||
|
||||
interface ApplicationFields {
|
||||
// Fields
|
||||
applicationTheme: ApplicationTheme;
|
||||
searchQuery: string;
|
||||
iconWeight: IconStyle;
|
||||
iconSize: number;
|
||||
iconColor: string;
|
||||
iconPreviewOpen: string | false;
|
||||
selectionEntry: IconEntry | null;
|
||||
filteredQueryResults: ReadonlyArray<IconEntry>;
|
||||
}
|
||||
|
||||
interface PersistedApplicationFields {
|
||||
searchQuery?: string;
|
||||
iconWeight?: IconStyle;
|
||||
iconSize?: number;
|
||||
iconColor?: string;
|
||||
}
|
||||
|
||||
export interface ApplicationState extends ApplicationFields {
|
||||
setSearchQuery: (query: string) => void;
|
||||
setIconWeight: (weight: IconStyle) => void;
|
||||
setIconSize: (size: number) => void;
|
||||
setIconColor: (color: string) => void;
|
||||
setIconPreviewOpen: (open: string | false) => void;
|
||||
setSelectionEntry: (entry: IconEntry | null) => void;
|
||||
resetApplicationState: () => void;
|
||||
}
|
||||
|
||||
export enum ApplicationTheme {
|
||||
LIGHT = "light",
|
||||
DARK = "dark",
|
||||
}
|
||||
|
||||
const fuse = new Fuse(icons, {
|
||||
keys: [{ name: "name", weight: 4 }, "tags", "categories", "codepoint"],
|
||||
threshold: 0.2,
|
||||
useExtendedSearch: true,
|
||||
});
|
||||
|
||||
const searchParameterStorage: PersistStorage<PersistedApplicationFields> = {
|
||||
getItem: (name) => {
|
||||
const params = new URLSearchParams(window.location.search);
|
||||
|
||||
let state: PersistedApplicationFields | null = null;
|
||||
switch (name) {
|
||||
case STORAGE_KEY:
|
||||
state = {
|
||||
iconWeight: parseWeight(params.get("weight")),
|
||||
iconSize: parseSize(params.get("size")),
|
||||
iconColor: parseColor(params.get("color")),
|
||||
searchQuery: parseQuery(params.get("q")),
|
||||
};
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
|
||||
return state === null ? null : { state };
|
||||
},
|
||||
setItem: (name, value) => {
|
||||
if (name === STORAGE_KEY) {
|
||||
const params = new URLSearchParams(window.location.search);
|
||||
if (value !== null) {
|
||||
for (const [k, v] of Object.entries(value.state)) {
|
||||
switch (k) {
|
||||
case "iconWeight": {
|
||||
if (v === IconStyle.REGULAR) {
|
||||
params.delete("weight");
|
||||
} else {
|
||||
params.set("weight", v);
|
||||
}
|
||||
break;
|
||||
}
|
||||
case "iconSize": {
|
||||
if (v === 32) {
|
||||
params.delete("size");
|
||||
} else {
|
||||
params.set("size", v.toString());
|
||||
}
|
||||
break;
|
||||
}
|
||||
case "iconColor": {
|
||||
if (v === "#000000") {
|
||||
params.delete("color");
|
||||
} else {
|
||||
params.set("color", v.replace("#", ""));
|
||||
}
|
||||
break;
|
||||
}
|
||||
case "searchQuery": {
|
||||
if (v === "") {
|
||||
params.delete("q");
|
||||
} else {
|
||||
params.set("q", v);
|
||||
}
|
||||
break;
|
||||
}
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
if (params.size === 0) {
|
||||
window.history.replaceState({}, "", window.location.pathname);
|
||||
} else {
|
||||
window.history.replaceState(
|
||||
{},
|
||||
"",
|
||||
`${window.location.pathname}?${params.toString()}`
|
||||
);
|
||||
}
|
||||
}
|
||||
},
|
||||
removeItem: (name) => {
|
||||
if (name !== STORAGE_KEY) return;
|
||||
const params = new URLSearchParams(window.location.search);
|
||||
params.delete("weight");
|
||||
params.delete("size");
|
||||
params.delete("color");
|
||||
params.delete("q");
|
||||
if (params.size === 0) {
|
||||
window.history.replaceState({}, "", window.location.pathname);
|
||||
} else {
|
||||
window.history.replaceState(
|
||||
{},
|
||||
"",
|
||||
`${window.location.pathname}?${params.toString()}`
|
||||
);
|
||||
}
|
||||
},
|
||||
};
|
||||
|
||||
export const useApplicationStore = createSelectors(
|
||||
create<ApplicationState>()(
|
||||
persist(
|
||||
(set) => {
|
||||
return {
|
||||
// Fields
|
||||
...initialState(),
|
||||
// Actions
|
||||
setSearchQuery: (searchQuery: string) => {
|
||||
const filteredQueryResults =
|
||||
searchQuery.trim() === ""
|
||||
? icons
|
||||
: fuse.search(searchQuery).map((value) => value.item);
|
||||
|
||||
set({ searchQuery, filteredQueryResults });
|
||||
},
|
||||
setIconWeight: (weight: IconStyle) => set({ iconWeight: weight }),
|
||||
setIconSize: (size: number) => set({ iconSize: size }),
|
||||
setIconColor: (color: string) => {
|
||||
const normalizedColor = TinyColor(color);
|
||||
if (normalizedColor.isValid()) {
|
||||
set({
|
||||
iconColor: normalizedColor.toHexString(),
|
||||
applicationTheme: normalizedColor.isLight()
|
||||
? ApplicationTheme.DARK
|
||||
: ApplicationTheme.LIGHT,
|
||||
});
|
||||
}
|
||||
},
|
||||
setIconPreviewOpen: (open: string | false) =>
|
||||
set({ iconPreviewOpen: open }),
|
||||
setSelectionEntry: (entry: IconEntry | null) =>
|
||||
set({ selectionEntry: entry }),
|
||||
resetApplicationState: () => {
|
||||
set({
|
||||
applicationTheme: ApplicationTheme.LIGHT,
|
||||
iconWeight: IconStyle.REGULAR,
|
||||
iconSize: 32,
|
||||
iconColor: "#000000",
|
||||
});
|
||||
},
|
||||
};
|
||||
},
|
||||
{
|
||||
name: STORAGE_KEY,
|
||||
storage: searchParameterStorage,
|
||||
partialize: (state): PersistedApplicationFields => ({
|
||||
searchQuery: state.searchQuery,
|
||||
iconWeight: state.iconWeight,
|
||||
iconSize: state.iconSize,
|
||||
iconColor: state.iconColor,
|
||||
}),
|
||||
}
|
||||
)
|
||||
)
|
||||
);
|
||||
|
||||
function initialState(): ApplicationFields {
|
||||
const params = new URLSearchParams(window.location.search);
|
||||
const searchQuery = parseQuery(params.get("q"));
|
||||
const iconWeight = parseWeight(params.get("weight"));
|
||||
const iconSize = parseSize(params.get("size"));
|
||||
const iconColor = parseColor(params.get("color"));
|
||||
|
||||
return {
|
||||
applicationTheme: TinyColor(iconColor).isLight()
|
||||
? ApplicationTheme.DARK
|
||||
: ApplicationTheme.LIGHT,
|
||||
searchQuery,
|
||||
iconWeight,
|
||||
iconSize,
|
||||
iconColor,
|
||||
iconPreviewOpen: false,
|
||||
selectionEntry: null,
|
||||
filteredQueryResults:
|
||||
searchQuery.trim() === ""
|
||||
? icons
|
||||
: fuse.search(searchQuery).map((value) => value.item),
|
||||
};
|
||||
}
|
||||
|
||||
type WithSelectors<S> = S extends { getState: () => infer T }
|
||||
? S & { use: { [K in keyof T]: () => T[K] } }
|
||||
: never;
|
||||
|
||||
function createSelectors<S extends UseBoundStore<StoreApi<object>>>(_store: S) {
|
||||
const store = _store as WithSelectors<typeof _store>;
|
||||
store.use = {};
|
||||
for (const k of Object.keys(store.getState())) {
|
||||
(store.use as any)[k] = () => store((s) => s[k as keyof typeof s]);
|
||||
}
|
||||
|
||||
return store;
|
||||
}
|
||||
|
||||
@@ -1,88 +0,0 @@
|
||||
import { selector, selectorFamily } from "recoil";
|
||||
import TinyColor from "tinycolor2";
|
||||
// @ts-ignore
|
||||
import Fuse from "fuse.js";
|
||||
import { IconCategory } from "@phosphor-icons/core";
|
||||
|
||||
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"],
|
||||
threshold: 0.2, // Tweak this to what feels like the right number of results
|
||||
// shouldSort: false,
|
||||
useExtendedSearch: true,
|
||||
});
|
||||
|
||||
export const filteredQueryResultsSelector = selector<ReadonlyArray<IconEntry>>({
|
||||
key: "filteredQueryResults",
|
||||
get: ({ get }) => {
|
||||
const query = get(searchQueryAtom).trim().toLowerCase();
|
||||
if (!query) return icons;
|
||||
|
||||
return new Promise((resolve) =>
|
||||
// @ts-ignore
|
||||
resolve(fuse.search(query).map((value) => value.item))
|
||||
);
|
||||
},
|
||||
});
|
||||
|
||||
type CategorizedIcons = Partial<Record<IconCategory, IconEntry[]>>;
|
||||
|
||||
export const categorizedQueryResultsSelector = selector<
|
||||
Readonly<CategorizedIcons>
|
||||
>({
|
||||
key: "categorizedQueryResults",
|
||||
get: ({ get }) => {
|
||||
const filteredResults = get(filteredQueryResultsSelector);
|
||||
return new Promise((resolve) =>
|
||||
resolve(
|
||||
filteredResults.reduce<CategorizedIcons>((acc, curr) => {
|
||||
curr.categories.forEach((category) => {
|
||||
if (!acc[category]) acc[category] = [];
|
||||
acc[category]!!.push(curr);
|
||||
});
|
||||
return acc;
|
||||
}, {})
|
||||
)
|
||||
);
|
||||
},
|
||||
});
|
||||
|
||||
export const singleCategoryQueryResultsSelector = selectorFamily<
|
||||
ReadonlyArray<IconEntry>,
|
||||
IconCategory
|
||||
>({
|
||||
key: "singleCategoryQueryResults",
|
||||
get:
|
||||
(category: IconCategory) =>
|
||||
({ get }) => {
|
||||
const filteredResults = get(filteredQueryResultsSelector);
|
||||
return new Promise((resolve) =>
|
||||
resolve(
|
||||
filteredResults.filter((icon) => icon.categories.includes(category))
|
||||
)
|
||||
);
|
||||
},
|
||||
});
|
||||
|
||||
export const isDarkThemeSelector = selector<boolean>({
|
||||
key: "isDarkTheme",
|
||||
get: ({ get }) => TinyColor(get(iconColorAtom)).isLight(),
|
||||
});
|
||||
|
||||
export const resetSettingsSelector = selector<null>({
|
||||
key: "resetSettings",
|
||||
get: () => null,
|
||||
set: ({ reset }) => {
|
||||
reset(iconWeightAtom);
|
||||
reset(iconSizeAtom);
|
||||
reset(iconColorAtom);
|
||||
},
|
||||
});
|
||||
@@ -30,29 +30,36 @@ export function getCodeSnippets({
|
||||
const { r, g, b } = TinyColor(color).toRgb();
|
||||
|
||||
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} :size="${size}" ${!isDefaultColor ? `color="${color}" ` : ""
|
||||
}${isDefaultWeight ? "" : `weight="${weight}" `}/>`,
|
||||
[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} :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.${camelName}${isDefaultWeight ? "" : " " + pascalWeight
|
||||
}
|
||||
)}${
|
||||
isDefaultWeight ? "" : weight.replace(/^\w/, (c) => c.toUpperCase())
|
||||
},\n size: ${size.toFixed(1)},\n${
|
||||
!isDefaultColor ? ` color: Color(0xff${color.replace("#", "")}),\n` : ""
|
||||
})`,
|
||||
[SnippetType.ELM]: `Phosphor.${camelName}${
|
||||
isDefaultWeight ? "" : " " + pascalWeight
|
||||
}
|
||||
|> withSize ${size}
|
||||
|> withSizeUnit "px"
|
||||
|> toHtml []`,
|
||||
[SnippetType.SWIFT]: `Ph.${camelName}.${weight}${!isDefaultColor
|
||||
[SnippetType.SWIFT]: `Ph.${camelName}.${weight}${
|
||||
!isDefaultColor
|
||||
? `\n .color(red: ${u8ToCGFloatStr(r)}, green: ${u8ToCGFloatStr(
|
||||
g
|
||||
)}, blue: ${u8ToCGFloatStr(b)})`
|
||||
g
|
||||
)}, blue: ${u8ToCGFloatStr(b)})`
|
||||
: ""
|
||||
}
|
||||
}
|
||||
.frame(width: ${size}, height: ${size})
|
||||
`,
|
||||
};
|
||||
@@ -68,3 +75,44 @@ export function supportsWeight({
|
||||
if (type !== SnippetType.FLUTTER) return true;
|
||||
return weight !== IconStyle.DUOTONE;
|
||||
}
|
||||
|
||||
export function stripWrappingQuotes(value: string | null | undefined): string {
|
||||
return value?.replace(/["'](.+)["']/, "$1") ?? "";
|
||||
}
|
||||
|
||||
export function parseWeight(weight: string | null | undefined): IconStyle {
|
||||
switch (stripWrappingQuotes(weight).toLowerCase()) {
|
||||
case "thin":
|
||||
return IconStyle.THIN;
|
||||
case "light":
|
||||
return IconStyle.LIGHT;
|
||||
case "bold":
|
||||
return IconStyle.BOLD;
|
||||
case "fill":
|
||||
return IconStyle.FILL;
|
||||
case "duotone":
|
||||
return IconStyle.DUOTONE;
|
||||
case "regular":
|
||||
default:
|
||||
return IconStyle.REGULAR;
|
||||
}
|
||||
}
|
||||
|
||||
export function parseQuery(query: string | null | undefined): string {
|
||||
return stripWrappingQuotes(query);
|
||||
}
|
||||
|
||||
export function parseSize(size: string | null | undefined): number {
|
||||
const sizeAsNumber = parseInt(stripWrappingQuotes(size) || "32", 10);
|
||||
return Number.isFinite(sizeAsNumber)
|
||||
? Math.min(Math.max(sizeAsNumber, 16), 96)
|
||||
: 32;
|
||||
}
|
||||
|
||||
export function parseColor(color: string | null | undefined): string {
|
||||
const parsedColor = TinyColor(stripWrappingQuotes(color) || "#000000");
|
||||
if (parsedColor.isValid()) {
|
||||
return parsedColor.toHexString();
|
||||
}
|
||||
return "#000000";
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user