49 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
rektdeckard
8b2bb2c0d3 chore(docs): alphabetize ports 2025-04-11 16:47:14 -06:00
Aral Balkan
aab567b24a Add Kitten to list of Phosphor icons ports
Phoshor icons are now available by default in Kitten.
2025-04-11 16:38:13 -06:00
JaJuMa
760afdccf9 Update README.md 2025-03-25 00:52:18 -06:00
JaJuMa
8b7b6a81a8 Update README.md 2025-03-25 00:52:18 -06:00
JaJuMa
f55dd1c728 Add jajuma/phosphorhyva port 2025-03-25 00:52:18 -06:00
rektdeckard
eba0540fb8 fix(issues): remove illegal whitespace in community_port issue 2025-03-25 00:33:21 -06:00
rektdeckard
d46bbd545a chore(issues): add community_port issue template 2025-03-25 00:28:37 -06:00
Tobias Fried
8d840b4859 chore(docs): use stylesheets and jsDelivr CDN in vanilla example 2025-03-22 14:51:15 -06:00
rektdeckard
2f55d3268d chore(docs): add some links 2025-01-30 02:14:18 -07:00
rektdeckard
e1af590aaa chore(meta): add opencollective sponsor link 2025-01-29 22:14:27 -07:00
rektdeckard
0cdb1211ff feat(app): add ko-fi to banner and FUNDING.yaml 2025-01-10 21:38:45 -07:00
rektdeckard
596ef0bc85 fix(ci): don't assume file encoding 2024-12-29 21:57:53 -07:00
rektdeckard
13248a3270 chore(ci): make sure to create synced files
...and intermediate directories if they don't already exist
2024-12-29 21:50:35 -07:00
rektdeckard
6d2358ffe7 feat(ci): remove meta folders 2024-12-29 21:30:48 -07:00
rektdeckard
e28695fcd2 feat(ci): sync additional files 2024-12-29 21:30:48 -07:00
rektdeckard
d05dd19606 chore(docs): adjust logo 2024-12-29 21:30:48 -07:00
rektdeckard
1635d050ea feat(docs): add community links and owner prefix 2024-12-29 21:30:48 -07:00
rektdeckard
a93f137fd3 chore(sketch): don't include unzipped asset 2024-12-29 01:09:41 -07:00
rektdeckard
bf1433a612 chore(app): use relative urls so testing and preview are live 2024-12-29 01:09:41 -07:00
rektdeckard
72486d8ba0 chore(sketch): update plugin bundle 2024-12-29 01:09:41 -07:00
rektdeckard
315c9df6de chore(docs): add penpot (#672) 2024-12-29 01:09:41 -07:00
rektdeckard
09461de291 chore(docs): sort links (mostly) alphabetically 2024-12-29 01:09:41 -07:00
rektdeckard
cf552f45fb chore(ci): re-disable except for readme changes on master 2024-12-29 01:09:41 -07:00
rektdeckard
bafb374af4 feat(ci): enable all repos and include link in PR 2024-12-29 01:09:41 -07:00
rektdeckard
31595b49cf chore(ci): allow synced repos to not have sections 2024-12-29 01:09:41 -07:00
rektdeckard
1f1b3450c1 fix(ci): use default branch 2024-12-29 01:09:41 -07:00
rektdeckard
3f25a8aa88 chore(ci): use matrix for repo list 2024-12-29 01:09:41 -07:00
rektdeckard
74811edda7 fix(ci): correct lockfile name 2024-12-29 01:09:41 -07:00
rektdeckard
a5e6f10b04 chore(ci): temporarily run on all path changes 2024-12-29 01:09:41 -07:00
rektdeckard
288f4466c5 chore(ci): tweak commit msg 2024-12-29 01:09:41 -07:00
rektdeckard
b6d47ab575 fix(ci): use repos list 2024-12-29 01:09:41 -07:00
rektdeckard
a4421c82b2 feat(ci): create doc PRs, maybe? 2024-12-29 01:09:41 -07:00
rektdeckard
5a390a9231 chore(meta): consistent yaml extensions lol 2024-12-29 01:09:41 -07:00
rektdeckard
625dc776d5 feat(ci): set up preview and prod deployments 2024-12-29 01:09:41 -07:00
rektdeckard
b99c50d61f chore(docs): use explicit version in icon font example (#657) 2024-12-23 22:22:23 -07:00
rektdeckard
363e86fada fix(a11y): use buttons for grid items 2024-12-05 00:55:13 -07:00
Quang Phan
72b6fe088f docs: add link to phosphor-icons-tailwindcss 2024-11-15 01:42:10 -07:00
Jason Pepas
97ac81b0c1 add pepaslabs/phosphor-uikit to README 2024-10-11 15:28:33 -06:00
Tobias Fried
090f4fe1fb chore(funding): native bmac 2024-09-30 00:49:20 -06:00
Tobias Fried
710518cdaa fix(docs): correct webfont usage 2024-09-17 14:41:15 -06:00
adamglin
1a8a7aefd6 Add compose-phosphor-icon 2024-08-22 14:06:22 -06:00
rektdeckard
443545f18b feat(app): update companies mentioned in footer 2024-06-10 12:51:12 -06:00
rektdeckard
83763a2beb fix(app): better color input appearance in Firefox 2024-06-10 12:49:45 -06:00
rektdeckard
dd3fc59e02 fix(app): update vue snippet syntax 2024-06-04 11:06:26 -06:00
43 changed files with 2027 additions and 1536 deletions

6
.github/FUNDING.yaml vendored Normal file
View File

@@ -0,0 +1,6 @@
# Thanks for considering supporting this project! 🎉
github: [phosphor-icons, rektdeckard]
open_collective: phosphoricons
ko_fi: phosphoricons
patreon: phosphoricons
buy_me_a_coffee: phosphoricons

3
.github/FUNDING.yml vendored
View File

@@ -1,3 +0,0 @@
github: [phosphor-icons, rektdeckard]
patreon: phosphoricons
custom: ["https://www.buymeacoffee.com/phosphoricons"]

View File

@@ -0,0 +1,15 @@
---
name: Community port
about: Add your Phosphor port to the list of community projects on all Phosphor repositories
title: ""
labels: documentation
assignees: rektdeckard
---
<!-- BEFORE YOU REQUEST -->
<!-- 1. Links to free, open source software only please. No paid services or upselling. -->
<!-- 2. If you are able to, please make a PR yourself adding your port (in alphabetical order) to the "Community Projects" section in README.md. -->
**Details**
<!-- What is the name of your port, and where can the source code or project page be found? -->

BIN
.github/logo.png vendored Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 16 KiB

View File

@@ -0,0 +1,53 @@
name: Build and deploy to preview
on:
push
concurrency:
group: 'preview'
cancel-in-progress: true
jobs:
deploy:
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Install pnpm
uses: pnpm/action-setup@v4
with:
run_install: false
- name: Install Node.js
uses: actions/setup-node@v4
with:
node-version: 20
cache: 'pnpm'
- name: Install dependencies
run: pnpm install
- name: Install sshpass
run: sudo apt-get install -y sshpass
- name: Add SSH Key to known_hosts
env:
KNOWN_HOSTS_ENTRY: ${{ secrets.DREAMHOST_KNOWN_HOSTS_ENTRY }}
HOST: ${{ secrets.HOST }}
run: |
mkdir -p ~/.ssh
echo "$KNOWN_HOSTS_ENTRY" >> ~/.ssh/known_hosts
chmod 644 ~/.ssh/known_hosts
- name: Build static site
run: pnpm build
- name: Deploy via rsync and sshpass
env:
USERNAME: ${{ secrets.DREAMHOST_FTP_USERNAME }}
PASSWORD: ${{ secrets.DREAMHOST_FTP_PASSWORD }}
HOST: ${{ secrets.DREAMHOST_FTP_HOST }}
DEPLOY_PATH: preview.phosphoricons.com
run: |
sshpass -p "$PASSWORD" rsync -avz --delete ./dist/* $USERNAME@$HOST:$DEPLOY_PATH

55
.github/workflows/dreamhost-static.yaml vendored Normal file
View File

@@ -0,0 +1,55 @@
name: Build and deploy to production
on:
push:
branches:
- master
concurrency:
group: 'prod'
cancel-in-progress: true
jobs:
deploy:
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Install pnpm
uses: pnpm/action-setup@v4
with:
run_install: false
- name: Install Node.js
uses: actions/setup-node@v4
with:
node-version: 20
cache: 'pnpm'
- name: Install dependencies
run: pnpm install
- name: Install sshpass
run: sudo apt-get install -y sshpass
- name: Add SSH Key to known_hosts
env:
KNOWN_HOSTS_ENTRY: ${{ secrets.DREAMHOST_KNOWN_HOSTS_ENTRY }}
HOST: ${{ secrets.HOST }}
run: |
mkdir -p ~/.ssh
echo "$KNOWN_HOSTS_ENTRY" >> ~/.ssh/known_hosts
chmod 644 ~/.ssh/known_hosts
- name: Build static site
run: pnpm build
- name: Deploy via rsync and sshpass
env:
USERNAME: ${{ secrets.DREAMHOST_FTP_USERNAME }}
PASSWORD: ${{ secrets.DREAMHOST_FTP_PASSWORD }}
HOST: ${{ secrets.DREAMHOST_FTP_HOST }}
DEPLOY_PATH: phosphoricons.com
run: |
sshpass -p "$PASSWORD" rsync -avz --delete ./dist/* $USERNAME@$HOST:$DEPLOY_PATH

100
.github/workflows/sync-docs.yaml vendored Normal file
View File

@@ -0,0 +1,100 @@
name: Sync documentation
on:
push:
paths:
- 'README.md'
- '.github/FUNDING.yaml'
- '.github/logo.png'
branches:
- master
workflow_dispatch: # Allows manual triggering
concurrency:
group: 'docs'
cancel-in-progress: true
jobs:
sync-docs:
runs-on: ubuntu-latest
strategy:
matrix:
repository: [
'phosphor-icons/core',
'phosphor-icons/figma',
'phosphor-icons/flutter',
'phosphor-icons/penpot',
'phosphor-icons/phosphor-elm',
'phosphor-icons/play',
'phosphor-icons/react',
'phosphor-icons/sketch',
'phosphor-icons/swift',
'phosphor-icons/theme',
'phosphor-icons/unplugin',
'phosphor-icons/vue',
'phosphor-icons/web',
'phosphor-icons/webcomponents'
]
steps:
- name: Checkout
uses: actions/checkout@v4
with:
path: source-repo
- name: Install pnpm
uses: pnpm/action-setup@v4
with:
version: 10
run_install: false
- name: Install Node.js
uses: actions/setup-node@v4
with:
node-version: 20
cache: 'pnpm'
cache-dependency-path: source-repo/pnpm-lock.yaml
- name: Install dependencies
working-directory: source-repo
run: pnpm install
- name: Sync to target repositories
env:
GITHUB_TOKEN: ${{ secrets.SYNC_PAT }}
run: |
echo "Syncing to ${{ matrix.repository }}"
# Get the source repository name and commit info
COMMIT_URL="https://github.com/${GITHUB_REPOSITORY}/commit/${GITHUB_SHA}"
# Clone target repository using HTTPS with token
git clone "https://x-access-token:${GITHUB_TOKEN}@github.com/${{ matrix.repository }}.git" target-repo
# Run sync script
cd source-repo
pnpm run sync-docs -- target-repo
cd ..
# Create PR if there are changes
cd target-repo
if [[ -n "$(git status --porcelain)" ]]; then
git config user.name "github-actions[bot]"
git config user.email "github-actions[bot]@users.noreply.github.com"
# Create branch
BRANCH="sync-readme-$(date +%Y%m%d-%H%M%S)"
git checkout -b $BRANCH
# Commit and push changes
git add .
git commit -am "chore(docs): sync readme section"
git push "https://x-access-token:${GITHUB_TOKEN}@github.com/${{ matrix.repository }}.git" $BRANCH
# Create PR using the GitHub CLI
gh pr create \
--repo "${{ matrix.repository }}" \
--title "chore(docs): sync readme section" \
--body "Automated PR to sync README section. This change originates from the following commit: ${COMMIT_URL}". \
--base $(git remote show origin | sed -n '/HEAD branch/s/.*: //p') \
--head $BRANCH
fi

View File

@@ -1,7 +1,10 @@
<img src="/meta/phosphor-mark-tight-black.png" width="96" align="right" />
# Phosphor Icons # Phosphor Icons
<!-- BEGIN_LOGO -->
<img src="/.github/logo.png" width="128" align="right" />
<!-- END_LOGO -->
<!-- BEGIN_OVERVIEW -->
Phosphor is a flexible icon family for interfaces, diagrams, presentations — whatever, really. Phosphor is a flexible icon family for interfaces, diagrams, presentations — whatever, really.
- 1,248 icons and counting - 1,248 icons and counting
@@ -10,6 +13,7 @@ Phosphor is a flexible icon family for interfaces, diagrams, presentations — w
- Raw stroke information retained to fine-tune the style - Raw stroke information retained to fine-tune the style
More ways to use at [phosphoricons.com](https://phosphoricons.com). More ways to use at [phosphoricons.com](https://phosphoricons.com).
<!-- END_OVERVIEW -->
## For developers ## For developers
@@ -17,18 +21,27 @@ Phosphor is available for [web](https://github.com/phosphor-icons/web), [React](
### Vanilla Web ### Vanilla Web
- **Simple to use** We use a similar approach as many other icon sets out there, providing icons as a webfont that uses Unicode's Private Use Area character codes to map normally non-rendering characters to icons. But you don't need to know that. All you need to do is add the script to the document `<head>`, and drop in icons with an `<i/>` tag and the appropriate class: - **Simple to use** We use a similar approach as many other icon sets out there, providing icons as a webfont that uses Unicode's Private Use Area character codes to map normally non-rendering characters to icons. But you don't need to know that. All you need to do is add the stylesheet for each weight you need to the document `<head>`, and drop in icons with an `<i/>` tag and the appropriate class:
```html ```html
<!doctype html> <!doctype html>
<html> <html>
<head> <head>
<script src="https://unpkg.com/@phosphor-icons/web"></script> <link
rel="stylesheet"
type="text/css"
href="https://cdn.jsdelivr.net/npm/@phosphor-icons/web@2.1.1/src/regular/style.css"
/>
<link
rel="stylesheet"
type="text/css"
href="https://cdn.jsdelivr.net/npm/@phosphor-icons/web@2.1.1/src/fill/style.css"
/>
</head> </head>
<body> <body>
<i class="ph-smiley"></i> <i class="ph ph-smiley"></i>
<i class="ph-fill ph-heart" style="color: hotpink"></i> <i class="ph-fill ph-heart" style="color: hotpink"></i>
<i class="ph-thin ph-cube"></i> <i class="ph ph-cube"></i>
</body> </body>
</html> </html>
``` ```
@@ -89,39 +102,61 @@ ReactDOM.render(<App />, document.getElementById("root"));
> [!NOTE] > [!NOTE]
> Due to possible namespace collisions with built-in HTML elements, compononent names in the Vue library are prefixed with `Ph`, but otherwise follow the same naming conventions. Both Pascal and kebab-case conventions can be used in templates. > Due to possible namespace collisions with built-in HTML elements, compononent names in the Vue library are prefixed with `Ph`, but otherwise follow the same naming conventions. Both Pascal and kebab-case conventions can be used in templates.
## Our Related Projects <!-- BEGIN_LINKS -->
## Our Projects
- [@phosphor-icons/homepage](https://github.com/phosphor-icons/homepage) ▲ Phosphor homepage and general info - [@phosphor-icons/homepage](https://github.com/phosphor-icons/homepage) ▲ Phosphor homepage and general info
- [@phosphor-icons/core](https://github.com/phosphor-icons/core) ▲ Phosphor icon assets and catalog - [@phosphor-icons/core](https://github.com/phosphor-icons/core) ▲ Phosphor icon assets and catalog
- [@phosphor-icons/react](https://github.com/phosphor-icons/react) ▲ Phosphor icon component library for React
- [@phosphor-icons/web](https://github.com/phosphor-icons/web) ▲ Phosphor icons for Vanilla JS
- [@phosphor-icons/vue](https://github.com/phosphor-icons/vue) ▲ Phosphor icon component library for Vue
- [@phosphor-icons/swift](https://github.com/phosphor-icons/swift) ▲ Phosphor icon component library for SwiftUI
- [@phosphor-icons/elm](https://github.com/phosphor-icons/phosphor-elm) ▲ Phosphor icons for Elm - [@phosphor-icons/elm](https://github.com/phosphor-icons/phosphor-elm) ▲ Phosphor icons for Elm
- [@phosphor-icons/flutter](https://github.com/phosphor-icons/flutter) ▲ Phosphor IconData library for Flutter
- [@phosphor-icons/webcomponents](https://github.com/phosphor-icons/webcomponents) ▲ Phosphor icons as Web Components
- [@phosphor-icons/figma](https://github.com/phosphor-icons/figma) ▲ Phosphor icons Figma plugin - [@phosphor-icons/figma](https://github.com/phosphor-icons/figma) ▲ Phosphor icons Figma plugin
- [@phosphor-icons/sketch](https://github.com/phosphor-icons/sketch) ▲ Phosphor icons Sketch plugin - [@phosphor-icons/flutter](https://github.com/phosphor-icons/flutter) ▲ Phosphor IconData library for Flutter
- [@phosphor-icons/pack](https://github.com/phosphor-icons/pack) ▲ Phosphor web font stripper to generate minimal icon bundles - [@phosphor-icons/pack](https://github.com/phosphor-icons/pack) ▲ Phosphor web font stripper to generate minimal icon bundles
- [@phosphor-icons/penpot](https://github.com/phosphor-icons/penpot) ▲ Phosphor icons Penpot plugin
- [@phosphor-icons/react](https://github.com/phosphor-icons/react) ▲ Phosphor icon component library for React
- [@phosphor-icons/sketch](https://github.com/phosphor-icons/sketch) ▲ Phosphor icons Sketch plugin
- [@phosphor-icons/swift](https://github.com/phosphor-icons/swift) ▲ Phosphor icon component library for SwiftUI
- [@phosphor-icons/theme](https://github.com/phosphor-icons/theme) ▲ A VS Code (and other IDE) theme with the Phosphor color palette - [@phosphor-icons/theme](https://github.com/phosphor-icons/theme) ▲ A VS Code (and other IDE) theme with the Phosphor color palette
- [@phosphor-icons/unplugin](https://github.com/phosphor-icons/unplugin) ▲ A multi-framework bundler plugin for generating Phosphor sprite sheets
- [@phosphor-icons/vue](https://github.com/phosphor-icons/vue) ▲ Phosphor icon component library for Vue
- [@phosphor-icons/web](https://github.com/phosphor-icons/web) ▲ Phosphor icons for Vanilla JS
- [@phosphor-icons/webcomponents](https://github.com/phosphor-icons/webcomponents) ▲ Phosphor icons as Web Components
## Community Projects ## Community Projects
- [phosphor-react-native](https://github.com/duongdev/phosphor-react-native) ▲ Phosphor icon component library for React Native - [adamglin0/compose-phosphor-icons](https://github.com/adamglin0/compose-phosphor-icon) ▲ Phosphor icons for Compose Multiplatform
- [phosphor-svelte](https://github.com/haruaki07/phosphor-svelte) ▲ Phosphor icons for Svelte apps - [altdsoy/phosphor_icons](https://github.com/altdsoy/phosphor_icons) ▲ Phosphor icons for Phoenix and TailwindCSS
- [phosphor-r](https://github.com/dreamRs/phosphoricons) ▲ Phosphor icon wrapper for R documents and applications - [amPerl/egui-phosphor](https://github.com/amperl/egui-phosphor) ▲ Phosphor icons for egui apps (Rust)
- [blade-phosphor-icons](https://github.com/codeat3/blade-phosphor-icons) ▲ Phosphor icons in your Laravel Blade views - [babakfp/phosphor-icons-svelte](https://github.com/babakfp/phosphor-icons-svelte) ▲ Phosphor icons for Svelte apps
- [brettkolodny/phosphor-lustre](https://github.com/brettkolodny/phosphor-lustre) ▲ Phosphor icons for Lustre
- [cellularmitosis/phosphor-uikit](https://github.com/cellularmitosis/phosphor-uikit) ▲ XCode asset catalog generator for Phosphor icons (Swift/UIKit)
- [cjohansen/phosphor-clj](https://github.com/cjohansen/phosphor-clj) ▲ Phosphor icons as Hiccup for Clojure and ClojureScript
- [codeat3/blade-phosphor-icons](https://github.com/codeat3/blade-phosphor-icons) ▲ Phosphor icons in your Laravel Blade views
- [dreamRs/phosphor-r](https://github.com/dreamRs/phosphoricons) ▲ Phosphor icon wrapper for R documents and applications
- [duongdev/phosphor-react-native](https://github.com/duongdev/phosphor-react-native) ▲ Phosphor icon component library for React Native
- [haruaki07/phosphor-svelte](https://github.com/haruaki07/phosphor-svelte) ▲ Phosphor icons for Svelte apps
- [IgnaceMaes/ember-phosphor-icons](https://github.com/IgnaceMaes/ember-phosphor-icons) ▲ Phosphor icons for Ember apps
- [iota-uz/icons](https://github.com/iota-uz/icons) ▲ Phosphor icons as Templ components (Go)
- [jajuma/phosphorhyva](https://github.com/JaJuMa-GmbH/phosphor-hyva) ▲ Phosphor icons for Magento 2 & Mage-OS with Hyvä Theme
- [Kitten](https://kitten.small-web.org/reference/#icons) ▲ Phosphor icons integrated by default in Kitten
- [lucagoslar/phosphor-css](https://github.com/lucagoslar/phosphor-css) ▲ CSS wrapper for Phosphor SVG icons
- [maful/ruby-phosphor-icons](https://github.com/maful/ruby-phosphor-icons) ▲ Phosphor icons for Ruby and Rails applications
- [meadowsys/phosphor-svgs](https://github.com/meadowsys/phosphor-svgs) ▲ Phosphor icons as Rust string constants
- [mwood/tamagui-phosphor-icons](https://github.com/mwood23/tamagui-phosphor-icons) ▲ Phosphor icons for Tamagui
- [noozo/phosphoricons_elixir](https://github.com/noozo/phosphoricons_elixir) ▲ Phosphor icons as SVG strings for Elixir/Phoenix
- [oyedejioyewole/nuxt-phosphor-icons](https://github.com/oyedejioyewole/nuxt-phosphor-icons) ▲ Phosphor icons integration for Nuxt
- [pepaslabs/phosphor-uikit](https://github.com/pepaslabs/phosphor-uikit) ▲ Xcode asset catalog generator for Swift/UIKit
- [raycast/phosphor-icons](https://www.raycast.com/marinsokol/phosphor-icons) ▲ Phosphor icons Raycast extension
- [reatlat/eleventy-plugin-phosphoricons](https://github.com/reatlat/eleventy-plugin-phosphoricons) ▲ An Eleventy shortcode plugin to embed icons as inline SVGs
- [robruiz/wordpress-phosphor-icons-block](https://github.com/robruiz/phosphor-icons-block) ▲ Phosphor icon block for use in WordPress v5.8+
- [sachaw/solid-phosphor](https://github.com/sachaw/solid-phosphor) ▲ Phosphor icons for SolidJS
- [SeanMcP/phosphor-astro](https://github.com/SeanMcP/phosphor-astro) ▲ Phosphor icons as Astro components
- [SorenHolstHansen/phosphor-leptos](https://github.com/SorenHolstHansen/phosphor-leptos) ▲ Phosphor icon component library for Leptos apps (Rust)
- [vnphanquang/phosphor-icons-tailwindcss](https://github.com/vnphanquang/phosphor-icons-tailwindcss) ▲ TailwindCSS plugin for Phosphor icons
- [wireui/phosphoricons](https://github.com/wireui/phosphoricons) ▲ Phosphor icons for Laravel - [wireui/phosphoricons](https://github.com/wireui/phosphoricons) ▲ Phosphor icons for Laravel
- [phosphor-css](https://github.com/lucagoslar/phosphor-css) ▲ CSS wrapper for Phosphor SVG icons
- [ruby-phosphor-icons](https://github.com/maful/ruby-phosphor-icons) ▲ Phosphor icons for Ruby and Rails applications
- [eleventy-plugin-phosphoricons](https://github.com/reatlat/eleventy-plugin-phosphoricons) ▲ An Eleventy plugin for add shortcode, allows Phosphor icons to be embedded as inline svg into templates
- [phosphor-leptos](https://github.com/SorenHolstHansen/phosphor-leptos) ▲ Phosphor icon component library for Leptos apps (rust)
- [wordpress-phosphor-icons-block](https://github.com/robruiz/phosphor-icons-block) ▲ Phosphor icon block for use in WordPress v5.8+
- [ember-phosphor-icons](https://github.com/IgnaceMaes/ember-phosphor-icons) ▲ Phosphor icons for Ember apps
If you've made a port of Phosphor and you want to see it here, just open a PR [here](https://github.com/phosphor-icons/homepage)! If you've made a port of Phosphor and you want to see it here, just open a PR [here](https://github.com/phosphor-icons/homepage)!
## License ## License
MIT © [Phosphor Icons](https://github.com/phosphor-icons) MIT © [Phosphor Icons](https://github.com/phosphor-icons)
<!-- END_LINKS -->

View File

@@ -1,6 +1,6 @@
{ {
"name": "@phosphor-icons/homepage", "name": "@phosphor-icons/homepage",
"version": "2.0.6", "version": "2.1.1",
"license": "MIT", "license": "MIT",
"homepage": "https://phosphoricons.com", "homepage": "https://phosphoricons.com",
"author": { "author": {
@@ -20,17 +20,18 @@
], ],
"repository": "github:phosphor-icons/homepage", "repository": "github:phosphor-icons/homepage",
"private": true, "private": true,
"packageManager": "pnpm@10.6.3",
"scripts": { "scripts": {
"dev": "vite", "dev": "vite",
"build": "tsc && vite build", "build": "tsc && vite build",
"preview": "vite preview", "preview": "vite preview",
"format": "prettier --write \"./src/**/*.{js,jsx,ts,tsx,json,vue}\"", "format": "prettier --write \"./src/**/*.{js,jsx,ts,tsx,json,vue}\"",
"generate": "tsx scripts/generate.ts" "generate": "tsx scripts/generate.ts",
"sync-docs": "tsx scripts/sync-docs.ts"
}, },
"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",
@@ -39,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",

2149
pnpm-lock.yaml generated

File diff suppressed because it is too large Load Diff

Binary file not shown.

72
scripts/sync-docs.ts Normal file
View File

@@ -0,0 +1,72 @@
import fs from "node:fs";
import path from "node:path";
const README_PATH = "README.md";
const FUNDING_PATH = ".github/FUNDING.yaml";
const LOGO_PATH = ".github/logo.png";
const SYNC_SECTIONS = ["LOGO", "OVERVIEW", "LINKS"];
const SYNC_FILES: Array<string | Array<string>> = [
[FUNDING_PATH, ".github/FUNDING.yml"],
[LOGO_PATH, "meta"],
]; // These files will be replaced in the target repository
(function main() {
const targetRepo = process.argv[process.argv.length - 1];
if (!targetRepo) throw new Error("Target repository not provided");
const readmePath = path.resolve(__dirname, `../${README_PATH}`);
const readmeContent = fs.readFileSync(readmePath, "utf8");
const targetReadmePath = path.resolve(__dirname, `../../${targetRepo}/${README_PATH}`);
if (!fs.existsSync(targetReadmePath)) throw new Error(`README.md not found in ${targetRepo}`);
for (const section of SYNC_SECTIONS) {
const readmeSection = extractSection(readmeContent, section);
if (readmeSection) {
const targetReadmeContent = fs.readFileSync(targetReadmePath, "utf8");
const updatedDocsContent = updateSection(targetReadmeContent, section, readmeSection);
fs.writeFileSync(targetReadmePath, updatedDocsContent);
}
}
for (const file of SYNC_FILES) {
const fileName = Array.isArray(file) ? file[0] : file;
const filePath = path.resolve(__dirname, `../${fileName}`);
const fileContent = fs.readFileSync(filePath);
// If target file has aliases, remove them
if (Array.isArray(file)) {
for (const alias of file) {
const targetPath = path.resolve(__dirname, `../../${targetRepo}/${alias}`);
if (fs.existsSync(targetPath)) {
fs.rmSync(targetPath, { recursive: true });
}
}
}
// Write the target file and intermediate directories, or overwrite if it already exists
const targetPath = path.resolve(__dirname, `../../${targetRepo}/${fileName}`);
if (!fs.existsSync(path.dirname(targetPath))) {
fs.mkdirSync(path.dirname(targetPath), { recursive: true });
}
fs.writeFileSync(targetPath, fileContent);
}
})();
function extractSection(content: string, section: string) {
const pattern = new RegExp(
`<!-- BEGIN_${section} -->\n([\\s\\S]*)\n<!-- END_${section} -->`,
"g",
);
const match = pattern.exec(content);
return match?.[1];
}
function updateSection(content: string, section: string, newContent: string) {
const pattern = new RegExp(
`<!-- BEGIN_${section} -->\n([\\s\\S]*)\n<!-- END_${section} -->`,
"g",
);
return content.replace(pattern, `<!-- BEGIN_${section} -->\n${newContent}\n<!-- END_${section} -->`);
}

View File

@@ -48,6 +48,10 @@ body {
background-color: var(--acid); background-color: var(--acid);
} }
::-moz-color-swatch {
border: none;
}
h2 { h2 {
font-weight: 400; font-weight: 400;
} }

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";
@@ -44,9 +44,9 @@ const Banner = ({ id, children, onClose }: BannerProps) => {
onClose onClose
? onClose(setBannerState) ? onClose(setBannerState)
: setBannerState((state) => ({ : setBannerState((state) => ({
...state, ...state,
seen: { ...state.seen, [id]: true }, seen: { ...state.seen, [id]: true },
})); }));
}; };
return ( return (
@@ -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>
@@ -65,6 +64,14 @@ const Footer = (_: FooterProps) => {
AllTrails AllTrails
</a> </a>
,{" "} ,{" "}
<a className="main-link" href="https://www.anthropic.com">
Anthropic
</a>
,{" "}
<a className="main-link" href="https://www.babbel.com/">
Babbel
</a>
,{" "}
<a <a
className="main-link" className="main-link"
href="https://www.dive.club/course/figma-academy" href="https://www.dive.club/course/figma-academy"
@@ -76,8 +83,8 @@ const Footer = (_: FooterProps) => {
Framer Framer
</a> </a>
,{" "} ,{" "}
<a className="main-link" href="https://www.outgo.co/"> <a className="main-link" href="https://www.khanacademy.org/">
Outgo Khan Academy
</a> </a>
,{" "} ,{" "}
<a <a
@@ -129,7 +136,7 @@ const Footer = (_: FooterProps) => {
) )
} }
> >
<Coffee size={24} /> <CoffeeIcon size={24} />
Buy us a coffee Buy us a coffee
</button> </button>
<button <button
@@ -142,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,26 +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 href="https://ko-fi.com/phosphoricons">
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!
@@ -83,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

@@ -16,6 +16,8 @@
.grid-item { .grid-item {
display: flex; display: flex;
appearance: none;
background: transparent;
box-sizing: border-box; box-sizing: border-box;
width: 160px; width: 160px;
height: 160px; height: 160px;

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*");
@@ -68,11 +71,10 @@ const IconGridItem = (props: IconGridItemProps) => {
}, [originOffset]); }, [originOffset]);
return ( return (
<motion.div <motion.button
className="grid-item" className="grid-item"
key={name} key={name}
ref={ref} ref={ref}
tabIndex={0}
style={{ style={{
...style, ...style,
backgroundColor: isOpen ? "var(--background-layer)" : undefined, backgroundColor: isOpen ? "var(--background-layer)" : undefined,
@@ -80,7 +82,6 @@ const IconGridItem = (props: IconGridItemProps) => {
custom={delayRef} custom={delayRef}
transition={transition} transition={transition}
variants={itemVariants} variants={itemVariants}
onKeyPress={(e) => e.key === "Enter" && handleOpen()}
onClick={handleOpen} onClick={handleOpen}
> >
<Icon /> <Icon />
@@ -89,7 +90,7 @@ const IconGridItem = (props: IconGridItemProps) => {
{isNew && <span className="badge new"></span>} {isNew && <span className="badge new"></span>}
{isUpdated && <span className="badge updated"></span>} {isUpdated && <span className="badge updated"></span>}
</p> </p>
</motion.div> </motion.button>
); );
}; };

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));
@@ -232,11 +227,11 @@ const Panel = () => {
navigator.clipboard?.writeText( navigator.clipboard?.writeText(
"data:image/svg+xml;base64," + "data:image/svg+xml;base64," +
btoa( btoa(
unescape( unescape(
encodeURIComponent(cloneWithSize(ref.current, size).outerHTML) encodeURIComponent(cloneWithSize(ref.current, size).outerHTML)
)
) )
)
); );
setCopied(CopyType.SVG_DATA); setCopied(CopyType.SVG_DATA);
}; };
@@ -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,20 +1,20 @@
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";
import "./Links.css"; import "./Links.css";
interface LinksProps {} interface LinksProps { }
const Links = (_: LinksProps) => { 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="https://phosphoricons.com/assets/phosphor-icons.zip" href="/assets/phosphor-icons.zip"
eventLabel="Download all" eventLabel="Download all"
download download
type="application/zip" type="application/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,9 +43,9 @@ const Links = (_: LinksProps) => {
</div> </div>
<div> <div>
<ArrowElbowDownRight size={24} /> <ArrowElbowDownRightIcon size={24} />
<OutboundLink <OutboundLink
href="https://phosphoricons.com/assets/phosphor-icons.sketchplugin.zip" href="/assets/phosphor-icons.sketchplugin.zip"
eventLabel="Download sketch plugin" eventLabel="Download sketch plugin"
download download
type="application/zip" type="application/zip"
@@ -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,9 +72,8 @@ 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)}
> >
{item.icon} {item.icon}

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,24 +13,20 @@ const root = createRoot(container!);
root.render( root.render(
<StrictMode> <StrictMode>
<RecoilRoot> <ErrorBoundary
<ErrorBoundary fallback={
fallback={ <Notice
<Notice message={
message={ <p>
<p> An error occurred. Try going{" "}
An error occurred. Try going{" "} <a href={window.location.origin}>home</a>.
<a href={window.location.origin}>home</a>. </p>
</p> }
} />
/> }
} >
> <App />
<RecoilURLSyncJSON location={{ part: "queryParams" }}> </ErrorBoundary>
<App />
</RecoilURLSyncJSON>
</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

@@ -36,9 +36,7 @@ export function getCodeSnippets({
[SnippetType.REACT]: `<${displayName} size={${size}} ${ [SnippetType.REACT]: `<${displayName} size={${size}} ${
!isDefaultColor ? `color="${color}" ` : "" !isDefaultColor ? `color="${color}" ` : ""
}${isDefaultWeight ? "" : `weight="${weight}" `}/>`, }${isDefaultWeight ? "" : `weight="${weight}" `}/>`,
[SnippetType.VUE]: `<ph${displayName [SnippetType.VUE]: `<Ph${displayName} :size="${size}" ${
.replace(/([a-z0-9]|(?=[A-Z]))([A-Z])/g, "$1-$2")
.toLowerCase()} :size="${size}" ${
!isDefaultColor ? `color="${color}" ` : "" !isDefaultColor ? `color="${color}" ` : ""
}${isDefaultWeight ? "" : `weight="${weight}" `}/>`, }${isDefaultWeight ? "" : `weight="${weight}" `}/>`,
[SnippetType.FLUTTER]: `Icon(\n PhosphorIcons.${displayName.replace( [SnippetType.FLUTTER]: `Icon(\n PhosphorIcons.${displayName.replace(
@@ -77,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";
}