5 Commits

Author SHA1 Message Date
rektdeckard
1ac0502868 chore(app): strip extraneous quotes when reading from searchparams 2025-05-21 18:11:31 -06:00
rektdeckard
9b2c2ffea2 chore(ci): update cy pnpm version 2025-05-17 17:59:05 -06:00
rektdeckard
e90584e282 feat(app): add rename notice banner 2025-05-17 17:59:05 -06:00
rektdeckard
ef83bdfb37 refactor(app): replace recoil with zustand 2025-05-17 17:59:05 -06:00
rektdeckard
7f613234e5 fix(docs): correct link to unplugin 2025-05-12 12:00:37 -06:00
34 changed files with 554 additions and 606 deletions

View File

@@ -17,7 +17,6 @@ jobs:
- name: Install pnpm - name: Install pnpm
uses: pnpm/action-setup@v4 uses: pnpm/action-setup@v4
with: with:
version: 9
run_install: false run_install: false
- name: Install Node.js - name: Install Node.js

View File

@@ -19,7 +19,6 @@ jobs:
- name: Install pnpm - name: Install pnpm
uses: pnpm/action-setup@v4 uses: pnpm/action-setup@v4
with: with:
version: 9
run_install: false run_install: false
- name: Install Node.js - name: Install Node.js

View File

@@ -44,7 +44,7 @@ jobs:
- name: Install pnpm - name: Install pnpm
uses: pnpm/action-setup@v4 uses: pnpm/action-setup@v4
with: with:
version: 9 version: 10
run_install: false run_install: false
- name: Install Node.js - name: Install Node.js

View File

@@ -20,7 +20,7 @@
], ],
"repository": "github:phosphor-icons/homepage", "repository": "github:phosphor-icons/homepage",
"private": true, "private": true,
"packageManager": "pnpm@9", "packageManager": "pnpm@10.6.3",
"scripts": { "scripts": {
"dev": "vite", "dev": "vite",
"build": "tsc && vite build", "build": "tsc && vite build",
@@ -31,8 +31,7 @@
}, },
"dependencies": { "dependencies": {
"@phosphor-icons/core": "^2.1.1", "@phosphor-icons/core": "^2.1.1",
"@phosphor-icons/react": "^2.1.4", "@phosphor-icons/react": "^2.1.8",
"@recoiljs/refine": "^0.1.1",
"file-saver": "^2.0.2", "file-saver": "^2.0.2",
"framer-motion": "^10.17.12", "framer-motion": "^10.17.12",
"fuse.js": "^6.4.1", "fuse.js": "^6.4.1",
@@ -41,10 +40,9 @@
"react-dropdown-select": "^4.4.2", "react-dropdown-select": "^4.4.2",
"react-ga4": "^2.1.0", "react-ga4": "^2.1.0",
"react-hotkeys-hook": "^4.4.3", "react-hotkeys-hook": "^4.4.3",
"recoil": "^0.7.7",
"recoil-sync": "^0.2.0",
"svg2png-converter": "^1.0.2", "svg2png-converter": "^1.0.2",
"tinycolor2": "^1.4.2" "tinycolor2": "^1.4.2",
"zustand": "^5.0.4"
}, },
"devDependencies": { "devDependencies": {
"@types/file-saver": "^2.0.5", "@types/file-saver": "^2.0.5",

132
pnpm-lock.yaml generated
View File

@@ -12,17 +12,14 @@ importers:
specifier: ^2.1.1 specifier: ^2.1.1
version: 2.1.1 version: 2.1.1
'@phosphor-icons/react': '@phosphor-icons/react':
specifier: ^2.1.4 specifier: ^2.1.8
version: 2.1.4(react-dom@18.2.0)(react@18.2.0) version: 2.1.8(react-dom@18.2.0(react@18.2.0))(react@18.2.0)
'@recoiljs/refine':
specifier: ^0.1.1
version: 0.1.1
file-saver: file-saver:
specifier: ^2.0.2 specifier: ^2.0.2
version: 2.0.5 version: 2.0.5
framer-motion: framer-motion:
specifier: ^10.17.12 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: fuse.js:
specifier: ^6.4.1 specifier: ^6.4.1
version: 6.6.2 version: 6.6.2
@@ -34,25 +31,22 @@ importers:
version: 18.2.0(react@18.2.0) version: 18.2.0(react@18.2.0)
react-dropdown-select: react-dropdown-select:
specifier: ^4.4.2 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: react-ga4:
specifier: ^2.1.0 specifier: ^2.1.0
version: 2.1.0 version: 2.1.0
react-hotkeys-hook: react-hotkeys-hook:
specifier: ^4.4.3 specifier: ^4.4.3
version: 4.4.3(react-dom@18.2.0)(react@18.2.0) version: 4.4.3(react-dom@18.2.0(react@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)
svg2png-converter: svg2png-converter:
specifier: ^1.0.2 specifier: ^1.0.2
version: 1.0.2 version: 1.0.2
tinycolor2: tinycolor2:
specifier: ^1.4.2 specifier: ^1.4.2
version: 1.6.0 version: 1.6.0
zustand:
specifier: ^5.0.4
version: 5.0.4(@types/react@18.2.47)(react@18.2.0)
devDependencies: devDependencies:
'@types/file-saver': '@types/file-saver':
specifier: ^2.0.5 specifier: ^2.0.5
@@ -71,7 +65,7 @@ importers:
version: 1.4.6 version: 1.4.6
'@vitejs/plugin-react': '@vitejs/plugin-react':
specifier: ^3.1.0 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: iconjar-exporter:
specifier: ^1.0.10 specifier: ^1.0.10
version: 1.0.10 version: 1.0.10
@@ -89,7 +83,7 @@ importers:
version: 4.5.2(@types/node@18.19.6) version: 4.5.2(@types/node@18.19.6)
vite-plugin-svgr: vite-plugin-svgr:
specifier: ^2.4.0 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: packages:
@@ -555,16 +549,13 @@ packages:
'@phosphor-icons/core@2.1.1': '@phosphor-icons/core@2.1.1':
resolution: {integrity: sha512-v4ARvrip4qBCImOE5rmPUylOEK4iiED9ZyKjcvzuezqMaiRASCHKcRIuvvxL/twvLpkfnEODCOJp5dM4eZilxQ==} resolution: {integrity: sha512-v4ARvrip4qBCImOE5rmPUylOEK4iiED9ZyKjcvzuezqMaiRASCHKcRIuvvxL/twvLpkfnEODCOJp5dM4eZilxQ==}
'@phosphor-icons/react@2.1.4': '@phosphor-icons/react@2.1.8':
resolution: {integrity: sha512-EeNwgcg1aeK5vG/JYVTullnZzk5zjftOrjLb1iYetyhn5ImFFREc+xG0FAB/dAY277DGDSF8pNdNbqG1SUKIxQ==} resolution: {integrity: sha512-RxJlAkErO+t50DsY82ga9RGOULK6Jux0MdmXqvDjtOzG3PYQFz6rjdUU2q06lPMMbJTT+d+qurKYmF7i2Uv74A==}
engines: {node: '>=10'} engines: {node: '>=10'}
peerDependencies: peerDependencies:
react: '>= 16.8' react: '>= 16.8'
react-dom: '>= 16.8' react-dom: '>= 16.8'
'@recoiljs/refine@0.1.1':
resolution: {integrity: sha512-ry02rHswJePYkH1o8K99qL4O6TBntF9/g7W5wXVwaOUrIJEZUGfl/I3+btPXbUgyyEZvNs5xcwvOw13AufmFQw==}
'@rollup/pluginutils@5.1.0': '@rollup/pluginutils@5.1.0':
resolution: {integrity: sha512-XTIWOPPcpvyKI6L1NHo0lFlCyznUEyPmPY1mc3KpPVDYulHSTvyeLNVW00QTLIAFNhR3kYnJTQHeGqU4M3n09g==} resolution: {integrity: sha512-XTIWOPPcpvyKI6L1NHo0lFlCyznUEyPmPY1mc3KpPVDYulHSTvyeLNVW00QTLIAFNhR3kYnJTQHeGqU4M3n09g==}
engines: {node: '>=14.0.0'} engines: {node: '>=14.0.0'}
@@ -806,9 +797,6 @@ packages:
resolution: {integrity: sha512-WOBp/EEGUiIsJSp7wcv/y6MO+lV9UoncWqxuFfm8eBwzWNgyfBd6Gz+IeKQ9jCmyhoH99g15M3T+QaVHFjizVA==} resolution: {integrity: sha512-WOBp/EEGUiIsJSp7wcv/y6MO+lV9UoncWqxuFfm8eBwzWNgyfBd6Gz+IeKQ9jCmyhoH99g15M3T+QaVHFjizVA==}
engines: {node: '>=4'} engines: {node: '>=4'}
hamt_plus@1.0.2:
resolution: {integrity: sha512-t2JXKaehnMb9paaYA7J0BX8QQAY8lwfQ9Gjf4pg/mk4krt+cmwmU652HOoWonf+7+EQV97ARPMhhVgU1ra2GhA==}
has-flag@3.0.0: has-flag@3.0.0:
resolution: {integrity: sha512-sKJf1+ceQBr4SMkvQnBDNDtf4TXpVhVGateu0t918bl30FnbE2m4vNLX+VWe/dpjlb+HugGYzW7uQXH98HPEYw==} resolution: {integrity: sha512-sKJf1+ceQBr4SMkvQnBDNDtf4TXpVhVGateu0t918bl30FnbE2m4vNLX+VWe/dpjlb+HugGYzW7uQXH98HPEYw==}
engines: {node: '>=4'} engines: {node: '>=4'}
@@ -944,23 +932,6 @@ packages:
resolution: {integrity: sha512-/3IjMdb2L9QbBdWiW5e3P2/npwMBaU9mHCSCUzNln0ZCYbcfTsGbTJrU/kGemdH2IWmB2ioZ+zkxtmq6g09fGQ==} resolution: {integrity: sha512-/3IjMdb2L9QbBdWiW5e3P2/npwMBaU9mHCSCUzNln0ZCYbcfTsGbTJrU/kGemdH2IWmB2ioZ+zkxtmq6g09fGQ==}
engines: {node: '>=0.10.0'} 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: regenerator-runtime@0.14.1:
resolution: {integrity: sha512-dYnhHh0nJoMfnkZs6GmmhFknAGRrLznOu5nc9ML+EJxGvrx6H7teuevqVqCuPcPK//3eDrrjQhehXVx9cnkGdw==} resolution: {integrity: sha512-dYnhHh0nJoMfnkZs6GmmhFknAGRrLznOu5nc9ML+EJxGvrx6H7teuevqVqCuPcPK//3eDrrjQhehXVx9cnkGdw==}
@@ -1019,10 +990,6 @@ packages:
resolution: {integrity: sha512-/OaKK0xYrs3DmxRYqL/yDc+FxFUVYhDlXMhRmv3z915w2HF1tnN1omB354j8VUGO/hbRzyD6Y3sA7v7GS/ceog==} resolution: {integrity: sha512-/OaKK0xYrs3DmxRYqL/yDc+FxFUVYhDlXMhRmv3z915w2HF1tnN1omB354j8VUGO/hbRzyD6Y3sA7v7GS/ceog==}
engines: {node: '>=4'} engines: {node: '>=4'}
transit-js@0.8.874:
resolution: {integrity: sha512-IDJJGKRzUbJHmN0P15HBBa05nbKor3r2MmG6aSt0UxXIlJZZKcddTk67/U7WyAeW9Hv/VYI02IqLzolsC4sbPA==}
engines: {node: '>= 0.10.0'}
tslib@2.6.2: tslib@2.6.2:
resolution: {integrity: sha512-AEYxH93jGFPn/a2iVAwW87VuUIkR1FVUKB77NwMF7nBTDkDrrT/Hpt/IrCJ0QXhW27jTBDcf5ZY7w6RiqTMw2Q==} resolution: {integrity: sha512-AEYxH93jGFPn/a2iVAwW87VuUIkR1FVUKB77NwMF7nBTDkDrrT/Hpt/IrCJ0QXhW27jTBDcf5ZY7w6RiqTMw2Q==}
@@ -1085,6 +1052,24 @@ packages:
resolution: {integrity: sha512-r3vXyErRCYJ7wg28yvBY5VSoAF8ZvlcW9/BwUzEtUsjvX/DKs24dIkuwjtuprwJJHsbyUbLApepYTR1BN4uHrg==} resolution: {integrity: sha512-r3vXyErRCYJ7wg28yvBY5VSoAF8ZvlcW9/BwUzEtUsjvX/DKs24dIkuwjtuprwJJHsbyUbLApepYTR1BN4uHrg==}
engines: {node: '>= 6'} 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: snapshots:
'@ampproject/remapping@2.2.1': '@ampproject/remapping@2.2.1':
@@ -1280,9 +1265,10 @@ snapshots:
'@emotion/use-insertion-effect-with-fallbacks': 1.0.1(react@18.2.0) '@emotion/use-insertion-effect-with-fallbacks': 1.0.1(react@18.2.0)
'@emotion/utils': 1.2.1 '@emotion/utils': 1.2.1
'@emotion/weak-memoize': 0.3.1 '@emotion/weak-memoize': 0.3.1
'@types/react': 18.2.47
hoist-non-react-statics: 3.3.2 hoist-non-react-statics: 3.3.2
react: 18.2.0 react: 18.2.0
optionalDependencies:
'@types/react': 18.2.47
'@emotion/serialize@1.1.3': '@emotion/serialize@1.1.3':
dependencies: dependencies:
@@ -1294,7 +1280,7 @@ snapshots:
'@emotion/sheet@1.2.2': {} '@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: dependencies:
'@babel/runtime': 7.23.8 '@babel/runtime': 7.23.8
'@emotion/babel-plugin': 11.11.0 '@emotion/babel-plugin': 11.11.0
@@ -1303,8 +1289,9 @@ snapshots:
'@emotion/serialize': 1.1.3 '@emotion/serialize': 1.1.3
'@emotion/use-insertion-effect-with-fallbacks': 1.0.1(react@18.2.0) '@emotion/use-insertion-effect-with-fallbacks': 1.0.1(react@18.2.0)
'@emotion/utils': 1.2.1 '@emotion/utils': 1.2.1
'@types/react': 18.2.47
react: 18.2.0 react: 18.2.0
optionalDependencies:
'@types/react': 18.2.47
'@emotion/unitless@0.8.1': {} '@emotion/unitless@0.8.1': {}
@@ -1470,18 +1457,18 @@ snapshots:
'@phosphor-icons/core@2.1.1': {} '@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: dependencies:
react: 18.2.0 react: 18.2.0
react-dom: 18.2.0(react@18.2.0) react-dom: 18.2.0(react@18.2.0)
'@recoiljs/refine@0.1.1': {} '@rollup/pluginutils@5.1.0(rollup@3.29.4)':
'@rollup/pluginutils@5.1.0':
dependencies: dependencies:
'@types/estree': 1.0.5 '@types/estree': 1.0.5
estree-walker: 2.0.2 estree-walker: 2.0.2
picomatch: 2.3.1 picomatch: 2.3.1
optionalDependencies:
rollup: 3.29.4
'@svgr/babel-plugin-add-jsx-attribute@6.5.1(@babel/core@7.23.7)': '@svgr/babel-plugin-add-jsx-attribute@6.5.1(@babel/core@7.23.7)':
dependencies: dependencies:
@@ -1578,7 +1565,7 @@ snapshots:
'@types/tinycolor2@1.4.6': {} '@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: dependencies:
'@babel/core': 7.23.7 '@babel/core': 7.23.7
'@babel/plugin-transform-react-jsx-self': 7.23.3(@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: {} 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: dependencies:
react: 18.2.0
react-dom: 18.2.0(react@18.2.0)
tslib: 2.6.2 tslib: 2.6.2
optionalDependencies: optionalDependencies:
'@emotion/is-prop-valid': 0.8.8 '@emotion/is-prop-valid': 0.8.8
react: 18.2.0
react-dom: 18.2.0(react@18.2.0)
fsevents@2.3.3: fsevents@2.3.3:
optional: true optional: true
@@ -1736,8 +1723,6 @@ snapshots:
globals@11.12.0: {} globals@11.12.0: {}
hamt_plus@1.0.2: {}
has-flag@3.0.0: {} has-flag@3.0.0: {}
hasown@2.0.0: hasown@2.0.0:
@@ -1830,10 +1815,10 @@ snapshots:
react: 18.2.0 react: 18.2.0
scheduler: 0.23.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: dependencies:
'@emotion/react': 11.11.0(@types/react@18.2.47)(react@18.2.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) '@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 prop-types: 15.8.1
react: 18.2.0 react: 18.2.0
react-dom: 18.2.0(react@18.2.0) react-dom: 18.2.0(react@18.2.0)
@@ -1842,7 +1827,7 @@ snapshots:
react-ga4@2.1.0: {} 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: dependencies:
react: 18.2.0 react: 18.2.0
react-dom: 18.2.0(react@18.2.0) react-dom: 18.2.0(react@18.2.0)
@@ -1855,18 +1840,6 @@ snapshots:
dependencies: dependencies:
loose-envify: 1.4.0 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: {} regenerator-runtime@0.14.1: {}
resolve-from@4.0.0: {} resolve-from@4.0.0: {}
@@ -1909,8 +1882,6 @@ snapshots:
to-fast-properties@2.0.0: {} to-fast-properties@2.0.0: {}
transit-js@0.8.874: {}
tslib@2.6.2: {} tslib@2.6.2: {}
tsx@4.7.3: tsx@4.7.3:
@@ -1930,9 +1901,9 @@ snapshots:
escalade: 3.1.1 escalade: 3.1.1
picocolors: 1.0.0 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: dependencies:
'@rollup/pluginutils': 5.1.0 '@rollup/pluginutils': 5.1.0(rollup@3.29.4)
'@svgr/core': 6.5.1 '@svgr/core': 6.5.1
vite: 4.5.2(@types/node@18.19.6) vite: 4.5.2(@types/node@18.19.6)
transitivePeerDependencies: transitivePeerDependencies:
@@ -1941,13 +1912,18 @@ snapshots:
vite@4.5.2(@types/node@18.19.6): vite@4.5.2(@types/node@18.19.6):
dependencies: dependencies:
'@types/node': 18.19.6
esbuild: 0.18.20 esbuild: 0.18.20
postcss: 8.4.33 postcss: 8.4.33
rollup: 3.29.4 rollup: 3.29.4
optionalDependencies: optionalDependencies:
'@types/node': 18.19.6
fsevents: 2.3.3 fsevents: 2.3.3
yallist@3.1.1: {} yallist@3.1.1: {}
yaml@1.10.2: {} 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

View File

@@ -1,5 +1,4 @@
import { Fragment, Suspense, useMemo } from "react"; import { Fragment, Suspense, useMemo } from "react";
import { useRecoilValue } from "recoil";
import "./App.css"; import "./App.css";
import Header from "@/components/Header"; import Header from "@/components/Header";
@@ -10,13 +9,13 @@ import ErrorBoundary from "@/components/ErrorBoundary";
import Notice from "@/components/Notice"; import Notice from "@/components/Notice";
// import Recipes from "@/components/Recipes"; // import Recipes from "@/components/Recipes";
import { useCSSVariables } from "@/hooks"; import { useCSSVariables } from "@/hooks";
import { isDarkThemeSelector } from "@/state"; import { ApplicationTheme, useApplicationStore } from "@/state";
const errorFallback = <Notice message="Search error" />; const errorFallback = <Notice message="Search error" />;
const waitingFallback = <Notice type="none" message="" />; const waitingFallback = <Notice type="none" message="" />;
const App: React.FC<any> = () => { const App: React.FC<any> = () => {
const isDark = useRecoilValue(isDarkThemeSelector); const isDark = useApplicationStore.use.applicationTheme() === ApplicationTheme.DARK;
useCSSVariables( useCSSVariables(
useMemo( useMemo(

View File

@@ -33,13 +33,13 @@
align-items: center; align-items: center;
justify-content: space-between; justify-content: space-between;
gap: 20px; gap: 20px;
max-width: 600px; max-width: 800px;
margin: auto; margin: auto;
padding: 12px 12px 12px 16px; padding: 12px 12px 12px 16px;
color: var(--moss); color: var(--moss);
background-color: var(--acid); background-color: var(--acid);
border: 1px solid var(--moss); border: 1px solid var(--moss);
border-radius: 32px; border-radius: 16px;
filter: drop-shadow(2px 2px 0 var(--moss-shadow)); filter: drop-shadow(2px 2px 0 var(--moss-shadow));
font-family: "IBM Plex Mono"; font-family: "IBM Plex Mono";
font-size: 14px; font-size: 14px;

View File

@@ -1,6 +1,6 @@
import { ReactNode, Dispatch, SetStateAction } from "react"; import { ReactNode, Dispatch, SetStateAction } from "react";
import { motion, AnimatePresence, Variants } from "framer-motion"; 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 ReactGA from "react-ga4";
import { useLocalStorage } from "@/hooks"; import { useLocalStorage } from "@/hooks";
@@ -69,7 +69,7 @@ const Banner = ({ id, children, onClose }: BannerProps) => {
e.key === "Enter" && handleClose(); e.key === "Enter" && handleClose();
}} }}
> >
<XCircle size={28} weight="regular" /> <XCircleIcon size={28} weight="regular" />
</button> </button>
</div> </div>
</motion.aside> </motion.aside>

View File

@@ -1,17 +1,20 @@
import { useCallback } from "react"; import { useCallback } from "react";
import { useRecoilState, useRecoilValue } from "recoil"; import { useShallow } from "zustand/react/shallow";
import { EyedropperSample } from "@phosphor-icons/react"; import { EyedropperSampleIcon } from "@phosphor-icons/react";
import { useThrottled } from "@/hooks"; import { useThrottled } from "@/hooks";
import { iconColorAtom, isDarkThemeSelector } from "@/state"; import { ApplicationTheme, useApplicationStore } from "@/state";
import "./ColorInput.css"; import "./ColorInput.css";
type ColorInputProps = {}; type ColorInputProps = {};
const ColorInput = (_: ColorInputProps) => { const ColorInput = (_: ColorInputProps) => {
const [color, setColor] = useRecoilState(iconColorAtom); const { color, setColor, theme } = useApplicationStore(useShallow((state) => ({
const isDark = useRecoilValue(isDarkThemeSelector); color: state.iconColor,
setColor: state.setIconColor,
theme: state.applicationTheme,
})));
const handleColorChange = useCallback( const handleColorChange = useCallback(
(event: React.ChangeEvent<HTMLInputElement>) => { (event: React.ChangeEvent<HTMLInputElement>) => {
@@ -46,9 +49,9 @@ const ColorInput = (_: ColorInputProps) => {
onChange={throttledColorChange} onChange={throttledColorChange}
value={color} value={color}
/> />
<span style={{ color: isDark ? "black" : "white" }}> <span style={{ color: theme === ApplicationTheme.DARK ? "black" : "white" }}>
{color === "currentColor" ? ( {color === "currentColor" ? (
<EyedropperSample size={28} weight="fill" /> <EyedropperSampleIcon size={28} weight="fill" />
) : ( ) : (
color color
)} )}

View File

@@ -1,13 +1,12 @@
import { useRecoilValue } from "recoil";
import { motion, AnimatePresence, Variants } from "framer-motion"; 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 Links from "@/components/Links/Links";
import { ReactComponent as RulerMarker } from "@/assets/ruler-marker.svg"; import { ReactComponent as RulerMarker } from "@/assets/ruler-marker.svg";
import { ReactComponent as RulerMarkerSpec } from "@/assets/ruler-marker-spec.svg"; import { ReactComponent as RulerMarkerSpec } from "@/assets/ruler-marker-spec.svg";
import { useMediaQuery } from "@/hooks"; import { useMediaQuery } from "@/hooks";
import { selectionEntryAtom } from "@/state"; import { useApplicationStore } from "@/state";
import "./Footer.css"; import "./Footer.css";
type FooterProps = {}; type FooterProps = {};
@@ -20,7 +19,7 @@ const variants: Variants = {
const Footer = (_: FooterProps) => { const Footer = (_: FooterProps) => {
const isMobile = useMediaQuery("(max-width: 719px)"); const isMobile = useMediaQuery("(max-width: 719px)");
const isViewing = !!useRecoilValue(selectionEntryAtom); const isViewing = !!useApplicationStore.use.selectionEntry();
return ( return (
<footer> <footer>
@@ -42,7 +41,7 @@ const Footer = (_: FooterProps) => {
?.scrollIntoView({ behavior: "smooth", block: "start" }); ?.scrollIntoView({ behavior: "smooth", block: "start" });
}} }}
> >
<ArrowULeftUp size="1em" /> <ArrowULeftUpIcon size="1em" />
</motion.button> </motion.button>
)} )}
</AnimatePresence> </AnimatePresence>
@@ -137,7 +136,7 @@ const Footer = (_: FooterProps) => {
) )
} }
> >
<Coffee size={24} /> <CoffeeIcon size={24} />
Buy us a coffee Buy us a coffee
</button> </button>
<button <button
@@ -150,7 +149,7 @@ const Footer = (_: FooterProps) => {
) )
} }
> >
<HandHeart size={24} /> <HandHeartIcon size={24} />
Become a patron Become a patron
</button> </button>
</div> </div>

View File

@@ -1,8 +1,8 @@
import { import {
ArrowCircleUpRight, ArrowCircleUpRightIcon,
ArrowCircleDown, ArrowCircleDownIcon,
MegaphoneSimple, MegaphoneSimpleIcon,
HandHeart, HandHeartIcon,
} from "@phosphor-icons/react"; } from "@phosphor-icons/react";
import Banner from "@/components/Banner"; import Banner from "@/components/Banner";
@@ -40,30 +40,31 @@ const Header = (_: HeaderProps) => {
return ( return (
<header> <header>
<Banner.Container> <Banner.Container>
<Banner id={"2.1.0"}> <Banner id={"rename-notice"}>
<div className="message"> <div className="message">
<MegaphoneSimple size={32} mirrored /> <MegaphoneSimpleIcon size={32} mirrored />
<small> <small>
Phosphor v2.1 is out, adding 268 new icons and some general 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{" "}
revisions. Check our{" "} <a href="https://github.com/phosphor-icons/react/releases/tag/v2.1.8">
<a href="https://github.com/phosphor-icons/homepage/releases">
release notes release notes
</a>{" "} </a>{" "}
to see what's changed! for details.
</small> </small>
</div> </div>
</Banner> </Banner>
<Banner id={"buymeacoffee2"}> <Banner id={"buymeacoffee2"}>
<div className="message"> <div className="message">
<HandHeart size={32} mirrored /> <HandHeartIcon size={32} mirrored />
<small> <small>
We are now processing donations via{" "} We are now processing donations via{" "}
<a href="https://www.buymeacoffee.com/phosphoricons"> <a href="https://www.buymeacoffee.com/phosphoricons">
Buy Me a Coffee Buy Me a Coffee
</a>{" "} </a>{", "}
and{" "}
<a href="https://ko-fi.com/phosphoricons"> <a href="https://ko-fi.com/phosphoricons">
Ko-fi Ko-fi
</a>{", and "}
<a href="https://opencollective.com/phosphoricons">
Open Collective
</a> </a>
! Your one-time or recurring contribution does a lot to keep us ! Your one-time or recurring contribution does a lot to keep us
going. Please show us some support if you can! going. Please show us some support if you can!
@@ -87,11 +88,11 @@ const Header = (_: HeaderProps) => {
</h2> </h2>
<div className="button-container"> <div className="button-container">
<button className="main-button" onClick={handleGetStarted}> <button className="main-button" onClick={handleGetStarted}>
<ArrowCircleUpRight size={24} /> <ArrowCircleUpRightIcon size={24} />
Get started Get started
</button> </button>
<button className="main-button" onClick={handleScrollToIcons}> <button className="main-button" onClick={handleScrollToIcons}>
<ArrowCircleDown size={24} /> <ArrowCircleDownIcon size={24} />
Explore icons Explore icons
</button> </button>
</div> </div>

View File

@@ -1,16 +1,8 @@
import { useRef, useEffect } from "react"; import { useRef, useEffect } from "react";
import { useRecoilValue } from "recoil";
import { motion, useAnimation } from "framer-motion"; import { motion, useAnimation } from "framer-motion";
import { IconContext } from "@phosphor-icons/react"; import { IconContext } from "@phosphor-icons/react";
import { import { ApplicationTheme, useApplicationStore } from "@/state";
iconWeightAtom,
iconSizeAtom,
iconColorAtom,
filteredQueryResultsSelector,
isDarkThemeSelector,
searchQueryAtom,
} from "@/state";
import Notice from "@/components/Notice"; import Notice from "@/components/Notice";
import Panel from "./Panel"; import Panel from "./Panel";
@@ -31,12 +23,14 @@ const defaultSearchTags = [
type IconGridProps = {}; type IconGridProps = {};
const IconGrid = (_: IconGridProps) => { const IconGrid = (_: IconGridProps) => {
const weight = useRecoilValue(iconWeightAtom); const {
const size = useRecoilValue(iconSizeAtom); iconWeight: weight,
const color = useRecoilValue(iconColorAtom); iconSize: size,
const isDark = useRecoilValue(isDarkThemeSelector); iconColor: color,
const query = useRecoilValue(searchQueryAtom); applicationTheme,
const filteredQueryResults = useRecoilValue(filteredQueryResultsSelector); filteredQueryResults,
searchQuery: query,
} = useApplicationStore();
const originOffset = useRef({ top: 0, left: 0 }); const originOffset = useRef({ top: 0, left: 0 });
const controls = useAnimation(); const controls = useAnimation();
@@ -74,7 +68,7 @@ const IconGrid = (_: IconGridProps) => {
<IconGridItem <IconGridItem
key={iconEntry.name} key={iconEntry.name}
index={index} index={index}
isDark={isDark} isDark={applicationTheme === ApplicationTheme.DARK}
entry={iconEntry} entry={iconEntry}
originOffset={originOffset} originOffset={originOffset}
/> />

View File

@@ -5,11 +5,11 @@ import {
MutableRefObject, MutableRefObject,
HTMLAttributes, HTMLAttributes,
} from "react"; } from "react";
import { useRecoilState } from "recoil";
import { motion } from "framer-motion"; import { motion } from "framer-motion";
import { useShallow } from "zustand/react/shallow";
import { IconEntry } from "@/lib"; import { IconEntry } from "@/lib";
import { selectionEntryAtom } from "@/state"; import { useApplicationStore } from "@/state";
interface IconGridItemProps extends HTMLAttributes<HTMLDivElement> { interface IconGridItemProps extends HTMLAttributes<HTMLDivElement> {
index: number; index: number;
@@ -33,7 +33,10 @@ const itemVariants = {
const IconGridItem = (props: IconGridItemProps) => { const IconGridItem = (props: IconGridItemProps) => {
const { index, originOffset, entry, style } = props; const { index, originOffset, entry, style } = props;
const { name, Icon } = entry; 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 isOpen = selection?.name === name;
const isNew = entry.tags.includes("*new*"); const isNew = entry.tags.includes("*new*");
const isUpdated = entry.tags.includes("*updated*"); const isUpdated = entry.tags.includes("*updated*");

View File

@@ -5,18 +5,17 @@ import React, {
useMemo, useMemo,
HTMLAttributes, HTMLAttributes,
} from "react"; } from "react";
import { useRecoilValue, useRecoilState } from "recoil";
import { useHotkeys } from "react-hotkeys-hook"; import { useHotkeys } from "react-hotkeys-hook";
import { motion, AnimatePresence, Variants } from "framer-motion"; import { motion, AnimatePresence, Variants } from "framer-motion";
import { Svg2Png } from "svg2png-converter"; import { Svg2Png } from "svg2png-converter";
import { saveAs } from "file-saver"; import { saveAs } from "file-saver";
import { import {
Copy, CopyIcon,
CheckCircle, CheckCircleIcon,
ArrowFatLinesDown, ArrowFatLinesDownIcon,
XCircle, XCircleIcon,
CaretDoubleLeft, CaretDoubleLeftIcon,
CaretDoubleRight, CaretDoubleRightIcon,
} from "@phosphor-icons/react"; } from "@phosphor-icons/react";
import { IconStyle } from "@phosphor-icons/core"; import { IconStyle } from "@phosphor-icons/core";
import ReactGA from "react-ga4"; import ReactGA from "react-ga4";
@@ -24,13 +23,7 @@ import ReactGA from "react-ga4";
import Tabs, { Tab } from "@/components/Tabs"; import Tabs, { Tab } from "@/components/Tabs";
import { useMediaQuery, useTransientState, useSessionStorage } from "@/hooks"; import { useMediaQuery, useTransientState, useSessionStorage } from "@/hooks";
import { SnippetType } from "@/lib"; import { SnippetType } from "@/lib";
import { import { useApplicationStore } from "@/state";
iconWeightAtom,
iconSizeAtom,
iconColorAtom,
selectionEntryAtom,
isDarkThemeSelector,
} from "@/state";
import { getCodeSnippets, supportsWeight } from "@/utils"; import { getCodeSnippets, supportsWeight } from "@/utils";
import TagCloud from "./TagCloud"; import TagCloud from "./TagCloud";
@@ -82,7 +75,7 @@ const ActionButton = (
} & HTMLAttributes<HTMLButtonElement> } & HTMLAttributes<HTMLButtonElement>
) => { ) => {
const { active, download, label, ...rest } = props; const { active, download, label, ...rest } = props;
const Icon = download ? ArrowFatLinesDown : Copy; const Icon = download ? ArrowFatLinesDownIcon : CopyIcon;
return ( return (
<button <button
{...rest} {...rest}
@@ -91,7 +84,7 @@ const ActionButton = (
tabIndex={0} tabIndex={0}
> >
{active ? ( {active ? (
<CheckCircle size={20} color="var(--olive)" weight="fill" /> <CheckCircleIcon size={20} color="var(--olive)" weight="fill" />
) : ( ) : (
<Icon size={20} color="currentColor" weight="fill" /> <Icon size={20} color="currentColor" weight="fill" />
)} )}
@@ -101,12 +94,14 @@ const ActionButton = (
}; };
const Panel = () => { 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>( const [copied, setCopied] = useTransientState<SnippetType | CopyType | false>(
false, false,
2000 2000
@@ -169,13 +164,13 @@ const Panel = () => {
onClick={(e) => handleCopySnippet(e, type)} onClick={(e) => handleCopySnippet(e, type)}
> >
{copied === type ? ( {copied === type ? (
<CheckCircle <CheckCircleIcon
size={20} size={20}
color="var(--olive)" color="var(--olive)"
weight="fill" weight="fill"
/> />
) : ( ) : (
<Copy size={20} color="var(--foreground)" weight="fill" /> <CopyIcon size={20} color="var(--foreground)" weight="fill" />
)} )}
</button> </button>
)} )}
@@ -187,7 +182,7 @@ const Panel = () => {
); );
return [snippets, tabs]; return [snippets, tabs];
}, [entry, weight, size, color, copied, isDark]); }, [entry, weight, size, color, copied]);
useHotkeys("esc", () => setSelectionEntry(null)); useHotkeys("esc", () => setSelectionEntry(null));
@@ -246,8 +241,7 @@ const Panel = () => {
const { name } = entry; const { name } = entry;
const data = await fetch( const data = await fetch(
`https://raw.githubusercontent.com/phosphor-icons/core/main/raw/${weight}/${name}${ `https://raw.githubusercontent.com/phosphor-icons/core/main/raw/${weight}/${name}${weight === "regular" ? "" : `-${weight}`
weight === "regular" ? "" : `-${weight}`
}.svg` }.svg`
); );
const content = await data.text(); const content = await data.text();
@@ -282,8 +276,7 @@ const Panel = () => {
const { name } = entry; const { name } = entry;
saveAs( saveAs(
`https://raw.githubusercontent.com/phosphor-icons/core/main/raw/${weight}/${name}${ `https://raw.githubusercontent.com/phosphor-icons/core/main/raw/${weight}/${name}${weight === "regular" ? "" : `-${weight}`
weight === "regular" ? "" : `-${weight}`
}.svg`, }.svg`,
`${entry?.name}${weight === "regular" ? "" : `-${weight}`}.svg` `${entry?.name}${weight === "regular" ? "" : `-${weight}`}.svg`
); );
@@ -433,13 +426,13 @@ const Panel = () => {
onClick={() => setShowMoreActions((s) => !s)} onClick={() => setShowMoreActions((s) => !s)}
> >
{!showMoreActions ? ( {!showMoreActions ? (
<CaretDoubleRight <CaretDoubleRightIcon
size={16} size={16}
weight="bold" weight="bold"
color="var(--foreground)" color="var(--foreground)"
/> />
) : ( ) : (
<CaretDoubleLeft <CaretDoubleLeftIcon
size={16} size={16}
weight="bold" weight="bold"
color="var(--foreground)" color="var(--foreground)"
@@ -459,7 +452,7 @@ const Panel = () => {
e.key === "Enter" && setSelectionEntry(null); e.key === "Enter" && setSelectionEntry(null);
}} }}
> >
<XCircle color="currentColor" size={28} weight="fill" /> <XCircleIcon color="currentColor" size={28} weight="fill" />
</button> </button>
</motion.aside> </motion.aside>
)} )}

View File

@@ -1,8 +1,7 @@
import { useCallback } from "react"; import { useCallback } from "react";
import { useSetRecoilState } from "recoil";
import { useMediaQuery } from "@/hooks"; import { useMediaQuery } from "@/hooks";
import { searchQueryAtom } from "@/state"; import { useApplicationStore } from "@/state";
import "./TagCloud.css"; import "./TagCloud.css";
interface TagCloudProps { interface TagCloudProps {
@@ -12,7 +11,7 @@ interface TagCloudProps {
const TagCloud = ({ name, tags }: TagCloudProps) => { const TagCloud = ({ name, tags }: TagCloudProps) => {
const isMobile = useMediaQuery("(max-width: 719px)"); const isMobile = useMediaQuery("(max-width: 719px)");
const setQuery = useSetRecoilState(searchQueryAtom); const setQuery = useApplicationStore.use.setSearchQuery();
const handleTagClick = useCallback( const handleTagClick = useCallback(
(tag: string) => { (tag: string) => {
setQuery(tag); setQuery(tag);

View File

@@ -1,4 +1,4 @@
import { ArrowElbowDownRight } from "@phosphor-icons/react"; import { ArrowElbowDownRightIcon } from "@phosphor-icons/react";
import { iconCount } from "@/lib/icons"; import { iconCount } from "@/lib/icons";
import OutboundLink from "@/components/OutboundLink"; import OutboundLink from "@/components/OutboundLink";
@@ -11,7 +11,7 @@ const Links = (_: LinksProps) => {
return ( return (
<div className="links"> <div className="links">
<div> <div>
<ArrowElbowDownRight size={24} /> <ArrowElbowDownRightIcon size={24} />
<OutboundLink <OutboundLink
className="nav-link" className="nav-link"
href="/assets/phosphor-icons.zip" href="/assets/phosphor-icons.zip"
@@ -24,7 +24,7 @@ const Links = (_: LinksProps) => {
</div> </div>
<div> <div>
<ArrowElbowDownRight size={24} /> <ArrowElbowDownRightIcon size={24} />
<span> <span>
<OutboundLink <OutboundLink
href="https://www.figma.com/community/plugin/898620911119764089/Phosphor-Icons" href="https://www.figma.com/community/plugin/898620911119764089/Phosphor-Icons"
@@ -43,7 +43,7 @@ const Links = (_: LinksProps) => {
</div> </div>
<div> <div>
<ArrowElbowDownRight size={24} /> <ArrowElbowDownRightIcon size={24} />
<OutboundLink <OutboundLink
href="/assets/phosphor-icons.sketchplugin.zip" href="/assets/phosphor-icons.sketchplugin.zip"
eventLabel="Download sketch plugin" eventLabel="Download sketch plugin"
@@ -55,7 +55,7 @@ const Links = (_: LinksProps) => {
</div> </div>
<div> <div>
<ArrowElbowDownRight size={24} /> <ArrowElbowDownRightIcon size={24} />
<OutboundLink <OutboundLink
href="https://play.phosphoricons.com" href="https://play.phosphoricons.com"
eventLabel="Showcase" eventLabel="Showcase"
@@ -65,7 +65,7 @@ const Links = (_: LinksProps) => {
</div> </div>
<div> <div>
<ArrowElbowDownRight size={24} /> <ArrowElbowDownRightIcon size={24} />
<OutboundLink <OutboundLink
href="https://github.com/phosphor-icons/homepage" href="https://github.com/phosphor-icons/homepage"
eventLabel="GitHub" eventLabel="GitHub"
@@ -74,7 +74,7 @@ const Links = (_: LinksProps) => {
</OutboundLink> </OutboundLink>
</div> </div>
<div> <div>
<ArrowElbowDownRight size={24} /> <ArrowElbowDownRightIcon size={24} />
<OutboundLink <OutboundLink
href="https://github.com/phosphor-icons/homepage/issues" href="https://github.com/phosphor-icons/homepage/issues"
eventLabel="Request" eventLabel="Request"
@@ -84,7 +84,7 @@ const Links = (_: LinksProps) => {
</div> </div>
<div> <div>
<ArrowElbowDownRight size={24} /> <ArrowElbowDownRightIcon size={24} />
<span> <span>
<OutboundLink <OutboundLink
href="https://www.buymeacoffee.com/phosphoricons" href="https://www.buymeacoffee.com/phosphoricons"
@@ -103,7 +103,7 @@ const Links = (_: LinksProps) => {
</div> </div>
<div> <div>
<ArrowElbowDownRight size={24} /> <ArrowElbowDownRightIcon size={24} />
<OutboundLink <OutboundLink
href="https://twitter.com/_phosphoricons" href="https://twitter.com/_phosphoricons"
eventLabel="Twitter" eventLabel="Twitter"

View File

@@ -1,6 +1,6 @@
import { ReactNode } from "react"; import { ReactNode } from "react";
import { motion } from "framer-motion"; import { motion } from "framer-motion";
import { HourglassMedium, Question, SmileyXEyes } from "@phosphor-icons/react"; import { HourglassMediumIcon, QuestionIcon, SmileyXEyesIcon } from "@phosphor-icons/react";
interface NoticeProps { interface NoticeProps {
message?: ReactNode; message?: ReactNode;
@@ -22,9 +22,9 @@ const Notice = ({
transition={{ duration: 0.5 }} transition={{ duration: 0.5 }}
> >
<div className="empty-list-box"> <div className="empty-list-box">
{type === "wait" && <HourglassMedium size={128} weight="fill" />} {type === "wait" && <HourglassMediumIcon size={128} weight="fill" />}
{type === "help" && <Question size={128} weight="fill" />} {type === "help" && <QuestionIcon size={128} weight="fill" />}
{type === "warn" && <SmileyXEyes size={128} weight="fill" />} {type === "warn" && <SmileyXEyesIcon size={128} weight="fill" />}
<p>{message}</p> <p>{message}</p>
{children} {children}
</div> </div>

View File

@@ -1,4 +1,4 @@
import { ArrowCircleUpRight } from "@phosphor-icons/react"; import { ArrowCircleUpRightIcon } from "@phosphor-icons/react";
export type RecipeProps = { export type RecipeProps = {
title: string; title: string;
@@ -12,7 +12,7 @@ const Recipe = ({ url, Example }: RecipeProps) => {
{/* <h1>{title}</h1> */} {/* <h1>{title}</h1> */}
<div className="recipe-linkout"> <div className="recipe-linkout">
<span>Open on StackBlitz</span> <span>Open on StackBlitz</span>
<ArrowCircleUpRight weight="fill" size={32} /> <ArrowCircleUpRightIcon weight="fill" size={32} />
</div> </div>
<Example /> <Example />
</a> </a>

View File

@@ -2,10 +2,10 @@ import { useMemo } from "react";
import { import {
Icon, Icon,
IconProps, IconProps,
Barricade, BarricadeIcon,
GasCan, GasCanIcon,
IceCream, IceCreamIcon,
FlyingSaucer, FlyingSaucerIcon,
} from "@phosphor-icons/react"; } from "@phosphor-icons/react";
import { RecipeProps } from "../Recipe"; import { RecipeProps } from "../Recipe";
@@ -51,10 +51,10 @@ const duocolor: RecipeProps = {
Example() { Example() {
return ( return (
<div className="example"> <div className="example">
<Duocolor Icon={FlyingSaucer} duocolor="darkcyan" /> <Duocolor Icon={FlyingSaucerIcon} duocolor="darkcyan" />
<Duocolor Icon={Barricade} color="darkgray" duocolor="orange" /> <Duocolor Icon={BarricadeIcon} color="darkgray" duocolor="orange" />
<Duocolor Icon={IceCream} color="saddlebrown" duocolor="lightpink" /> <Duocolor Icon={IceCreamIcon} color="saddlebrown" duocolor="lightpink" />
<Duocolor Icon={GasCan} duocolor="indianred" /> <Duocolor Icon={GasCanIcon} duocolor="indianred" />
</div> </div>
); );
}, },

View File

@@ -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"; import { RecipeProps } from "../Recipe";
@@ -8,7 +8,7 @@ const gradient: RecipeProps = {
Example() { Example() {
return ( return (
<div className="example"> <div className="example">
<Fire weight="fill" color="url(#flame)"> <FireIcon weight="fill" color="url(#flame)">
<defs> <defs>
<linearGradient id="flame" x1="0%" y1="100%" x2="0%" y2="0%"> <linearGradient id="flame" x1="0%" y1="100%" x2="0%" y2="0%">
<stop offset="10%" stopColor="#FFDB00" /> <stop offset="10%" stopColor="#FFDB00" />
@@ -18,9 +18,9 @@ const gradient: RecipeProps = {
<stop offset="95%" stopColor="#E25822" /> <stop offset="95%" stopColor="#E25822" />
</linearGradient> </linearGradient>
</defs> </defs>
</Fire> </FireIcon>
<RainbowCloud color="url(#spectrum)"> <RainbowCloudIcon color="url(#spectrum)">
<defs> <defs>
<linearGradient id="spectrum"> <linearGradient id="spectrum">
<stop offset="10%" stopColor="indigo" /> <stop offset="10%" stopColor="indigo" />
@@ -30,9 +30,9 @@ const gradient: RecipeProps = {
<stop offset="95%" stopColor="red" /> <stop offset="95%" stopColor="red" />
</linearGradient> </linearGradient>
</defs> </defs>
</RainbowCloud> </RainbowCloudIcon>
<Peace weight="fill" color="url(#spectrum2)"> <PeaceIcon weight="fill" color="url(#spectrum2)">
<defs> <defs>
<radialGradient id="spectrum2"> <radialGradient id="spectrum2">
<stop offset="15%" stopColor="indigo" /> <stop offset="15%" stopColor="indigo" />
@@ -42,16 +42,16 @@ const gradient: RecipeProps = {
<stop offset="95%" stopColor="red" /> <stop offset="95%" stopColor="red" />
</radialGradient> </radialGradient>
</defs> </defs>
</Peace> </PeaceIcon>
<Image color="url(#sunset)" weight="fill"> <ImageIcon color="url(#sunset)" weight="fill">
<defs> <defs>
<linearGradient id="sunset" x1="0%" y1="100%" x2="100%" y2="0%"> <linearGradient id="sunset" x1="0%" y1="100%" x2="100%" y2="0%">
<stop offset="0%" stopColor="violet" /> <stop offset="0%" stopColor="violet" />
<stop offset="100%" stopColor="yellow" /> <stop offset="100%" stopColor="yellow" />
</linearGradient> </linearGradient>
</defs> </defs>
</Image> </ImageIcon>
</div> </div>
); );
}, },

View File

@@ -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"; import { RecipeProps } from "../Recipe";
@@ -8,7 +8,7 @@ const animation: RecipeProps = {
Example() { Example() {
return ( return (
<div className="example"> <div className="example">
<CassetteTape <CassetteTapeIcon
color="teal" color="teal"
style={{ filter: "url(#displacementFilter)" }} style={{ filter: "url(#displacementFilter)" }}
> >
@@ -29,10 +29,10 @@ const animation: RecipeProps = {
/> />
</filter> </filter>
</defs> </defs>
</CassetteTape> </CassetteTapeIcon>
<Cube color="teal" style={{ filter: "url(#displacementFilter)" }} /> <CubeIcon color="teal" style={{ filter: "url(#displacementFilter)" }} />
<ThumbsUp color="teal" style={{ filter: "url(#displacementFilter)" }} /> <ThumbsUpIcon color="teal" style={{ filter: "url(#displacementFilter)" }} />
<Virus color="teal" style={{ filter: "url(#displacementFilter)" }} /> <VirusIcon color="teal" style={{ filter: "url(#displacementFilter)" }} />
</div> </div>
); );
}, },

View File

@@ -5,19 +5,19 @@ import {
MutableRefObject, MutableRefObject,
ReactNode, ReactNode,
} from "react"; } from "react";
import { useRecoilState } from "recoil"; import { useShallow } from "zustand/react/shallow";
import { useHotkeys } from "react-hotkeys-hook"; import { useHotkeys } from "react-hotkeys-hook";
import { import {
Command, CommandIcon,
MagnifyingGlass, MagnifyingGlassIcon,
X, XIcon,
HourglassHigh, HourglassHighIcon,
} from "@phosphor-icons/react"; } from "@phosphor-icons/react";
import ReactGA from "react-ga4"; import ReactGA from "react-ga4";
import { useDebounce } from "@/hooks"; import { useDebounce } from "@/hooks";
import { searchQueryAtom } from "@/state";
import "./SearchInput.css"; import "./SearchInput.css";
import { useApplicationStore } from "@/state";
const apple = /iPhone|iPod|iPad|Macintosh|MacIntel|MacPPC/i; const apple = /iPhone|iPod|iPad|Macintosh|MacIntel|MacPPC/i;
const isApple = apple.test(window.navigator.platform); const isApple = apple.test(window.navigator.platform);
@@ -29,7 +29,10 @@ type SearchInputProps = {};
const SearchInput = (_: SearchInputProps) => { const SearchInput = (_: SearchInputProps) => {
const [value, setValue] = useState<string>(""); const [value, setValue] = useState<string>("");
const [query, setQuery] = useRecoilState(searchQueryAtom); const { query, setQuery } = useApplicationStore(useShallow((state) => ({
query: state.searchQuery,
setQuery: state.setSearchQuery,
})));
const inputRef = const inputRef =
useRef<HTMLInputElement>() as MutableRefObject<HTMLInputElement>; useRef<HTMLInputElement>() as MutableRefObject<HTMLInputElement>;
@@ -77,7 +80,7 @@ const SearchInput = (_: SearchInputProps) => {
return ( return (
<div className="search-bar"> <div className="search-bar">
<MagnifyingGlass id="search-icon" size={24} /> <MagnifyingGlassIcon id="search-icon" size={24} />
<input <input
ref={inputRef} ref={inputRef}
id="search-input" id="search-input"
@@ -93,12 +96,12 @@ const SearchInput = (_: SearchInputProps) => {
(key === "Enter" || key === "Escape") && currentTarget.blur() (key === "Enter" || key === "Escape") && currentTarget.blur()
} }
/> />
{!value && !isMobile && <Keys>{isApple ? <Command /> : "Ctrl + "}K</Keys>} {!value && !isMobile && <Keys>{isApple ? <CommandIcon /> : "Ctrl + "}K</Keys>}
{value ? ( {value ? (
value === query ? ( 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} ) : null}
</div> </div>

View File

@@ -1,29 +1,27 @@
import { useState } from "react"; import { useState } from "react";
import ReactGA from "react-ga4"; import ReactGA from "react-ga4";
import { useRecoilState, useResetRecoilState, useSetRecoilState } from "recoil"; import { useShallow } from "zustand/react/shallow";
import { import {
ArrowCounterClockwise, ArrowCounterClockwiseIcon,
CheckCircle, CheckCircleIcon,
DiceFive, DiceFiveIcon,
Link, LinkIcon,
} from "@phosphor-icons/react"; } from "@phosphor-icons/react";
import { IconStyle } from "@phosphor-icons/core"; import { IconStyle } from "@phosphor-icons/core";
import { useTransientState } from "@/hooks"; import { useTransientState } from "@/hooks";
import { import { useApplicationStore } from "@/state";
iconWeightAtom,
iconSizeAtom,
iconColorAtom,
resetSettingsSelector,
} from "@/state";
import "./SettingsActions.css"; import "./SettingsActions.css";
const SettingsActions = () => { const SettingsActions = () => {
const [weight, setWeight] = useRecoilState(iconWeightAtom); const { weight, setWeight, setSize, setColor, reset } = useApplicationStore(useShallow((state) => ({
const setSize = useSetRecoilState(iconSizeAtom); weight: state.iconWeight,
const setColor = useSetRecoilState(iconColorAtom); setWeight: state.setIconWeight,
const reset = useResetRecoilState(resetSettingsSelector); setSize: state.setIconSize,
setColor: state.setIconColor,
reset: state.resetApplicationState,
})));
const [copied, setCopied] = useTransientState<boolean>(false, 2000); const [copied, setCopied] = useTransientState<boolean>(false, 2000);
const [booped, setBooped] = useState<boolean>(false); const [booped, setBooped] = useState<boolean>(false);
@@ -67,7 +65,7 @@ const SettingsActions = () => {
title="Restore default settings" title="Restore default settings"
onClick={reset} onClick={reset}
> >
<ArrowCounterClockwise size={24} /> <ArrowCounterClockwiseIcon size={24} />
</button> </button>
<button <button
className="tool-button" className="tool-button"
@@ -75,9 +73,9 @@ const SettingsActions = () => {
onClick={copyDeepLinkToClipboard} onClick={copyDeepLinkToClipboard}
> >
{copied ? ( {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>
<button <button
@@ -90,7 +88,7 @@ const SettingsActions = () => {
style={{ display: "flex" }} style={{ display: "flex" }}
onAnimationEnd={() => setBooped(false)} onAnimationEnd={() => setBooped(false)}
> >
<DiceFive className={booped ? "spin" : ""} size={24} /> <DiceFiveIcon className={booped ? "spin" : ""} size={24} />
</span> </span>
</button> </button>
</> </>

View File

@@ -1,7 +1,7 @@
import React, { useCallback } from "react"; 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"; import "./SizeInput.css";
type SizeInputProps = {}; type SizeInputProps = {};
@@ -15,7 +15,10 @@ const handleBlur = (event: React.UIEvent<HTMLInputElement>) => {
}; };
const SizeInput = (_: SizeInputProps) => { const SizeInput = (_: SizeInputProps) => {
const [size, setSize] = useRecoilState(iconSizeAtom); const { size, setSize } = useApplicationStore(useShallow((state) => ({
size: state.iconSize,
setSize: state.setIconSize,
})));
const handleSizeChange = useCallback( const handleSizeChange = useCallback(
(event: React.ChangeEvent<HTMLInputElement>) => { (event: React.ChangeEvent<HTMLInputElement>) => {

View File

@@ -1,10 +1,9 @@
import { useMemo } from "react"; import { useShallow } from "zustand/react/shallow";
import { useRecoilState } from "recoil";
import Select from "react-dropdown-select"; 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 { IconStyle } from "@phosphor-icons/core";
import { iconWeightAtom } from "@/state"; import { useApplicationStore } from "@/state";
import "./StyleInput.css"; import "./StyleInput.css";
@@ -14,44 +13,45 @@ const options: WeightOption[] = [
{ {
key: "Thin", key: "Thin",
value: IconStyle.THIN, value: IconStyle.THIN,
icon: <PencilSimpleLine size={24} weight="thin" />, icon: <PencilSimpleLineIcon size={24} weight="thin" />,
}, },
{ {
key: "Light", key: "Light",
value: IconStyle.LIGHT, value: IconStyle.LIGHT,
icon: <PencilSimpleLine size={24} weight="light" />, icon: <PencilSimpleLineIcon size={24} weight="light" />,
}, },
{ {
key: "Regular", key: "Regular",
value: IconStyle.REGULAR, value: IconStyle.REGULAR,
icon: <PencilSimpleLine size={24} weight="regular" />, icon: <PencilSimpleLineIcon size={24} weight="regular" />,
}, },
{ {
key: "Bold", key: "Bold",
value: IconStyle.BOLD, value: IconStyle.BOLD,
icon: <PencilSimpleLine size={24} weight="bold" />, icon: <PencilSimpleLineIcon size={24} weight="bold" />,
}, },
{ {
key: "Fill", key: "Fill",
value: IconStyle.FILL, value: IconStyle.FILL,
icon: <PencilSimpleLine size={24} weight="fill" />, icon: <PencilSimpleLineIcon size={24} weight="fill" />,
}, },
{ {
key: "Duotone", key: "Duotone",
value: IconStyle.DUOTONE, value: IconStyle.DUOTONE,
icon: <PencilSimpleLine size={24} weight="duotone" />, icon: <PencilSimpleLineIcon size={24} weight="duotone" />,
}, },
]; ];
type StyleInputProps = {}; type StyleInputProps = {};
const StyleInput = (_: StyleInputProps) => {
const [style, setStyle] = useRecoilState(iconWeightAtom);
const currentStyle = useMemo( const StyleInput = (_: StyleInputProps) => {
() => [options.find((option) => option.value === style)!!], const { style, setStyle } = useApplicationStore(useShallow((state) => ({
[style] style: state.iconWeight,
); setStyle: state.setIconWeight,
})));
const currentStyle = [options.find((option) => option.value === style)!];
const handleStyleChange = (values: WeightOption[]) => const handleStyleChange = (values: WeightOption[]) =>
setStyle(values[0].value as IconStyle); setStyle(values[0].value as IconStyle);
@@ -72,8 +72,7 @@ const StyleInput = (_: StyleInputProps) => {
<span <span
role="option" role="option"
aria-selected={item.key === values[0].key} aria-selected={item.key === values[0].key}
className={`react-dropdown-select-item ${ className={`react-dropdown-select-item ${itemIndex === cursor ? "react-dropdown-select-item-active" : ""
itemIndex === cursor ? "react-dropdown-select-item-active" : ""
}`} }`}
onClick={() => methods.addItem(item)} onClick={() => methods.addItem(item)}
> >

View File

@@ -3,7 +3,7 @@ export { default as useDebounce } from "./useDebounce";
export { default as useEvent } from "./useEvent"; export { default as useEvent } from "./useEvent";
export { default as useLocalStorage } from "./useLocalStorage"; export { default as useLocalStorage } from "./useLocalStorage";
export { default as useMediaQuery } from "./useMediaQuery"; 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 useSessionStorage } from "./useSessionStorage";
export { default as useThrottle } from "./useThrottle"; export { default as useThrottle } from "./useThrottle";
export { default as useThrottled } from "./useThrottled"; export { default as useThrottled } from "./useThrottled";

View File

@@ -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]
);
}

View File

@@ -1,7 +1,5 @@
import { StrictMode } from "react"; import { StrictMode } from "react";
import { createRoot } from "react-dom/client"; import { createRoot } from "react-dom/client";
import { RecoilRoot } from "recoil";
import { RecoilURLSyncJSON } from "recoil-sync";
import App from "./components/App"; import App from "./components/App";
import ReactGA from "react-ga4"; import ReactGA from "react-ga4";
import ErrorBoundary from "./components/ErrorBoundary"; import ErrorBoundary from "./components/ErrorBoundary";
@@ -15,7 +13,6 @@ const root = createRoot(container!);
root.render( root.render(
<StrictMode> <StrictMode>
<RecoilRoot>
<ErrorBoundary <ErrorBoundary
fallback={ fallback={
<Notice <Notice
@@ -28,11 +25,8 @@ root.render(
/> />
} }
> >
<RecoilURLSyncJSON location={{ part: "queryParams" }}>
<App /> <App />
</RecoilURLSyncJSON>
</ErrorBoundary> </ErrorBoundary>
</RecoilRoot>
</StrictMode> </StrictMode>
); );

View File

@@ -1,5 +1,6 @@
import { Icon } from "@phosphor-icons/react"; import { Icon } from "@phosphor-icons/react";
import { IconEntry as CoreEntry } from "@phosphor-icons/core"; import { IconEntry as CoreEntry } from "@phosphor-icons/core";
export * from "./icons";
export interface IconEntry extends CoreEntry { export interface IconEntry extends CoreEntry {
Icon: Icon; Icon: Icon;

View File

@@ -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);
}

View File

@@ -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,
});

View File

@@ -1,4 +1,238 @@
export * from "./atoms"; import Fuse from "fuse.js";
export * from "./selectors"; 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__"; 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;
}

View File

@@ -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);
},
});

View File

@@ -30,24 +30,31 @@ export function getCodeSnippets({
const { r, g, b } = TinyColor(color).toRgb(); const { r, g, b } = TinyColor(color).toRgb();
return { return {
[SnippetType.HTML]: `<i class="ph${isDefaultWeight ? "" : `-${weight}` [SnippetType.HTML]: `<i class="ph${
isDefaultWeight ? "" : `-${weight}`
} ph-${name}"></i>`, } ph-${name}"></i>`,
[SnippetType.REACT]: `<${displayName} size={${size}} ${!isDefaultColor ? `color="${color}" ` : "" [SnippetType.REACT]: `<${displayName} size={${size}} ${
!isDefaultColor ? `color="${color}" ` : ""
}${isDefaultWeight ? "" : `weight="${weight}" `}/>`, }${isDefaultWeight ? "" : `weight="${weight}" `}/>`,
[SnippetType.VUE]: `<Ph${displayName} :size="${size}" ${!isDefaultColor ? `color="${color}" ` : "" [SnippetType.VUE]: `<Ph${displayName} :size="${size}" ${
!isDefaultColor ? `color="${color}" ` : ""
}${isDefaultWeight ? "" : `weight="${weight}" `}/>`, }${isDefaultWeight ? "" : `weight="${weight}" `}/>`,
[SnippetType.FLUTTER]: `Icon(\n PhosphorIcons.${displayName.replace( [SnippetType.FLUTTER]: `Icon(\n PhosphorIcons.${displayName.replace(
/^\w/, /^\w/,
(c) => c.toLowerCase() (c) => c.toLowerCase()
)}${isDefaultWeight ? "" : weight.replace(/^\w/, (c) => c.toUpperCase()) )}${
},\n size: ${size.toFixed(1)},\n${!isDefaultColor ? ` color: Color(0xff${color.replace("#", "")}),\n` : "" 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 [SnippetType.ELM]: `Phosphor.${camelName}${
isDefaultWeight ? "" : " " + pascalWeight
} }
|> withSize ${size} |> withSize ${size}
|> withSizeUnit "px" |> withSizeUnit "px"
|> toHtml []`, |> toHtml []`,
[SnippetType.SWIFT]: `Ph.${camelName}.${weight}${!isDefaultColor [SnippetType.SWIFT]: `Ph.${camelName}.${weight}${
!isDefaultColor
? `\n .color(red: ${u8ToCGFloatStr(r)}, green: ${u8ToCGFloatStr( ? `\n .color(red: ${u8ToCGFloatStr(r)}, green: ${u8ToCGFloatStr(
g g
)}, blue: ${u8ToCGFloatStr(b)})` )}, blue: ${u8ToCGFloatStr(b)})`
@@ -68,3 +75,44 @@ export function supportsWeight({
if (type !== SnippetType.FLUTTER) return true; if (type !== SnippetType.FLUTTER) return true;
return weight !== IconStyle.DUOTONE; 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";
}