Compare commits
52 Commits
dependabot
...
master
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
1ac0502868 | ||
|
|
9b2c2ffea2 | ||
|
|
e90584e282 | ||
|
|
ef83bdfb37 | ||
|
|
7f613234e5 | ||
|
|
8b2bb2c0d3 | ||
|
|
aab567b24a | ||
|
|
760afdccf9 | ||
|
|
8b7b6a81a8 | ||
|
|
f55dd1c728 | ||
|
|
eba0540fb8 | ||
|
|
d46bbd545a | ||
|
|
8d840b4859 | ||
|
|
2f55d3268d | ||
|
|
e1af590aaa | ||
|
|
0cdb1211ff | ||
|
|
596ef0bc85 | ||
|
|
13248a3270 | ||
|
|
6d2358ffe7 | ||
|
|
e28695fcd2 | ||
|
|
d05dd19606 | ||
|
|
1635d050ea | ||
|
|
a93f137fd3 | ||
|
|
bf1433a612 | ||
|
|
72486d8ba0 | ||
|
|
315c9df6de | ||
|
|
09461de291 | ||
|
|
cf552f45fb | ||
|
|
bafb374af4 | ||
|
|
31595b49cf | ||
|
|
1f1b3450c1 | ||
|
|
3f25a8aa88 | ||
|
|
74811edda7 | ||
|
|
a5e6f10b04 | ||
|
|
288f4466c5 | ||
|
|
b6d47ab575 | ||
|
|
a4421c82b2 | ||
|
|
5a390a9231 | ||
|
|
625dc776d5 | ||
|
|
b99c50d61f | ||
|
|
363e86fada | ||
|
|
72b6fe088f | ||
|
|
97ac81b0c1 | ||
|
|
090f4fe1fb | ||
|
|
710518cdaa | ||
|
|
1a8a7aefd6 | ||
|
|
443545f18b | ||
|
|
83763a2beb | ||
|
|
dd3fc59e02 | ||
|
|
b376d2517e | ||
|
|
91fee139e8 | ||
|
|
f608c6ea4f |
6
.github/FUNDING.yaml
vendored
Normal file
6
.github/FUNDING.yaml
vendored
Normal 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
3
.github/FUNDING.yml
vendored
@@ -1,3 +0,0 @@
|
|||||||
github: [phosphor-icons, rektdeckard]
|
|
||||||
patreon: phosphoricons
|
|
||||||
custom: ["https://www.buymeacoffee.com/phosphoricons"]
|
|
||||||
15
.github/ISSUE_TEMPLATE/community_port.md
vendored
Normal file
15
.github/ISSUE_TEMPLATE/community_port.md
vendored
Normal 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
BIN
.github/logo.png
vendored
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 16 KiB |
53
.github/workflows/dreamhost-preview-static.yaml
vendored
Normal file
53
.github/workflows/dreamhost-preview-static.yaml
vendored
Normal 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
55
.github/workflows/dreamhost-static.yaml
vendored
Normal 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
100
.github/workflows/sync-docs.yaml
vendored
Normal 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
|
||||||
85
README.md
85
README.md
@@ -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>
|
||||||
```
|
```
|
||||||
@@ -86,34 +99,64 @@ ReactDOM.render(<App />, document.getElementById("root"));
|
|||||||
</script>
|
</script>
|
||||||
```
|
```
|
||||||
|
|
||||||
> **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.
|
> [!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.
|
||||||
|
|
||||||
## Our Related Projects
|
<!-- BEGIN_LINKS -->
|
||||||
|
## Our Projects
|
||||||
|
|
||||||
|
- [@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/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/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/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/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/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)
|
|
||||||
|
|
||||||
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 -->
|
||||||
|
|||||||
14
package.json
14
package.json
@@ -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
2149
pnpm-lock.yaml
generated
File diff suppressed because it is too large
Load Diff
BIN
public/assets/phosphor-icons.sketchplugin.zip
Normal file
BIN
public/assets/phosphor-icons.sketchplugin.zip
Normal file
Binary file not shown.
Binary file not shown.
63510
public/assets/phosphor.nucleo.json
Normal file
63510
public/assets/phosphor.nucleo.json
Normal file
File diff suppressed because it is too large
Load Diff
@@ -1,8 +1,8 @@
|
|||||||
import path from "node:path";
|
|
||||||
import fs from "node:fs/promises";
|
import fs from "node:fs/promises";
|
||||||
|
import path from "node:path";
|
||||||
import { fileURLToPath } from "node:url";
|
import { fileURLToPath } from "node:url";
|
||||||
import IconJar, { IconGroup, IconSet, Icon, License } from "iconjar-exporter";
|
|
||||||
import { icons, IconStyle } from "@phosphor-icons/core";
|
import { icons, IconStyle } from "@phosphor-icons/core";
|
||||||
|
import IconJar, { IconGroup, IconSet, Icon, License } from "iconjar-exporter";
|
||||||
|
|
||||||
const __filename = fileURLToPath(import.meta.url);
|
const __filename = fileURLToPath(import.meta.url);
|
||||||
const __dirname = path.dirname(__filename);
|
const __dirname = path.dirname(__filename);
|
||||||
@@ -23,6 +23,54 @@ abstract class Exporter {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
class NucleoExporter implements Exporter {
|
||||||
|
static JSON_PATH = path.join(OUT_DIR, "./phosphor.nucleo.json");
|
||||||
|
static SET_ID = 1;
|
||||||
|
static data: {
|
||||||
|
sets: Array<{ label: string; id: number }>;
|
||||||
|
groups: Array<unknown>;
|
||||||
|
icons: Array<{
|
||||||
|
name: string;
|
||||||
|
content: string;
|
||||||
|
style: /* possibly just "outline" and "glyph" */ string;
|
||||||
|
tags: /* comma-separated, no spaces */ string;
|
||||||
|
set_id: number;
|
||||||
|
}>;
|
||||||
|
};
|
||||||
|
|
||||||
|
static async load(): Promise<void> {
|
||||||
|
NucleoExporter.data = {
|
||||||
|
sets: [{ label: "Phosphor Icons", id: NucleoExporter.SET_ID }],
|
||||||
|
groups: [],
|
||||||
|
icons: [],
|
||||||
|
};
|
||||||
|
|
||||||
|
for (const weight of Object.values(IconStyle)) {
|
||||||
|
for (const icon of icons) {
|
||||||
|
const name =
|
||||||
|
weight === "regular" ? icon.name : `${icon.name}-${weight}`;
|
||||||
|
const filePath = path.join(CORE_DIR, `${weight}/${name}.svg`);
|
||||||
|
const content = (await fs.readFile(filePath)).toString();
|
||||||
|
|
||||||
|
NucleoExporter.data.icons.push({
|
||||||
|
name,
|
||||||
|
content,
|
||||||
|
style: "outline",
|
||||||
|
tags: icon.tags.join(","),
|
||||||
|
set_id: NucleoExporter.SET_ID,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
static async save(): Promise<void> {
|
||||||
|
await fs.writeFile(
|
||||||
|
NucleoExporter.JSON_PATH,
|
||||||
|
JSON.stringify(NucleoExporter.data)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
class IconJarExporter implements Exporter {
|
class IconJarExporter implements Exporter {
|
||||||
static iconJar: IconJar;
|
static iconJar: IconJar;
|
||||||
static JAR_PATH = path.join(OUT_DIR, "./phosphor.iconjar");
|
static JAR_PATH = path.join(OUT_DIR, "./phosphor.iconjar");
|
||||||
@@ -68,7 +116,7 @@ class IconJarExporter implements Exporter {
|
|||||||
}
|
}
|
||||||
|
|
||||||
(async function main() {
|
(async function main() {
|
||||||
for (const exporter of [IconJarExporter]) {
|
for (const exporter of [IconJarExporter, NucleoExporter]) {
|
||||||
await exporter.load();
|
await exporter.load();
|
||||||
await exporter.save();
|
await exporter.save();
|
||||||
}
|
}
|
||||||
|
|||||||
72
scripts/sync-docs.ts
Normal file
72
scripts/sync-docs.ts
Normal 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} -->`);
|
||||||
|
}
|
||||||
@@ -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;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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(
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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
|
||||||
)}
|
)}
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
@@ -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}
|
||||||
/>
|
/>
|
||||||
|
|||||||
@@ -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>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -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>
|
||||||
)}
|
)}
|
||||||
|
|||||||
@@ -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);
|
||||||
|
|||||||
@@ -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"
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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>
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -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>
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -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>
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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>
|
||||||
</>
|
</>
|
||||||
|
|||||||
@@ -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>) => {
|
||||||
|
|||||||
@@ -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}
|
||||||
|
|||||||
@@ -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";
|
||||||
|
|||||||
@@ -1,23 +0,0 @@
|
|||||||
import { useRecoilValue } from "recoil";
|
|
||||||
import {
|
|
||||||
iconWeightAtom,
|
|
||||||
iconSizeAtom,
|
|
||||||
iconColorAtom,
|
|
||||||
STORAGE_KEY,
|
|
||||||
} from "@/state";
|
|
||||||
import useDebounce from "./useDebounce";
|
|
||||||
|
|
||||||
export default function usePersistSettings() {
|
|
||||||
const weight = useRecoilValue(iconWeightAtom);
|
|
||||||
const size = useRecoilValue(iconSizeAtom);
|
|
||||||
const color = useRecoilValue(iconColorAtom);
|
|
||||||
|
|
||||||
useDebounce(
|
|
||||||
() => {
|
|
||||||
const serializedState = JSON.stringify({ weight, size, color });
|
|
||||||
window.localStorage.setItem(STORAGE_KEY, serializedState);
|
|
||||||
},
|
|
||||||
2000,
|
|
||||||
[weight, size, color]
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@@ -1,7 +1,5 @@
|
|||||||
import { StrictMode } from "react";
|
import { 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>
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
@@ -1,85 +0,0 @@
|
|||||||
import { useCallback, ReactNode } from "react";
|
|
||||||
import { DefaultValue } from "recoil";
|
|
||||||
import { ReadItem, WriteItems, ListenToItems, RecoilSync } from "recoil-sync";
|
|
||||||
import { STORAGE_KEY } from ".";
|
|
||||||
|
|
||||||
const DEFAULT_VALUE = new DefaultValue();
|
|
||||||
|
|
||||||
export default ({ children }: { children: ReactNode }) => {
|
|
||||||
const read: ReadItem = useCallback((itemKey) => {
|
|
||||||
if (typeof document === "undefined") return DEFAULT_VALUE; // SSR
|
|
||||||
|
|
||||||
const item = localStorage.getItem(itemKey);
|
|
||||||
|
|
||||||
let parsed: unknown;
|
|
||||||
|
|
||||||
try {
|
|
||||||
parsed = item === null ? DEFAULT_VALUE : parseJSON(item);
|
|
||||||
} catch {
|
|
||||||
parsed = DEFAULT_VALUE;
|
|
||||||
console.warn({ itemKey, item }, "parseJSON failed");
|
|
||||||
}
|
|
||||||
|
|
||||||
return parsed;
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
const write: WriteItems = useCallback(({ diff }) => {
|
|
||||||
if (typeof document === "undefined") return; // SSR
|
|
||||||
|
|
||||||
for (const [key, value] of diff) {
|
|
||||||
if (value instanceof DefaultValue) {
|
|
||||||
localStorage.removeItem(key);
|
|
||||||
} else {
|
|
||||||
// reasons for setItem to fail: https://developer.mozilla.org/en-US/docs/Web/API/Storage/setItem#exceptions
|
|
||||||
try {
|
|
||||||
localStorage.setItem(key, JSON.stringify(value));
|
|
||||||
} catch (err) {
|
|
||||||
console.warn({ err, key, value }, "localStorage.setItem failed");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
const listen: ListenToItems = useCallback(
|
|
||||||
({ updateItem, updateAllKnownItems }) => {
|
|
||||||
void updateAllKnownItems;
|
|
||||||
const onStorage = (event: StorageEvent) => {
|
|
||||||
// ignore clear() calls
|
|
||||||
if (event.storageArea === localStorage && event.key !== null) {
|
|
||||||
let parsed: unknown;
|
|
||||||
try {
|
|
||||||
parsed =
|
|
||||||
event.newValue === null
|
|
||||||
? DEFAULT_VALUE
|
|
||||||
: parseJSON(event.newValue);
|
|
||||||
} catch {
|
|
||||||
parsed = DEFAULT_VALUE;
|
|
||||||
console.warn({ event }, "parseJSON failed");
|
|
||||||
}
|
|
||||||
|
|
||||||
updateItem(event.key, parsed);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
window.addEventListener("storage", onStorage);
|
|
||||||
|
|
||||||
return () => window.removeEventListener("storage", onStorage);
|
|
||||||
},
|
|
||||||
[]
|
|
||||||
);
|
|
||||||
|
|
||||||
return (
|
|
||||||
<RecoilSync
|
|
||||||
storeKey={STORAGE_KEY}
|
|
||||||
read={read}
|
|
||||||
write={write}
|
|
||||||
listen={listen}
|
|
||||||
>
|
|
||||||
{children}
|
|
||||||
</RecoilSync>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
function parseJSON(value: string): unknown {
|
|
||||||
return value === "undefined" ? undefined : JSON.parse(value);
|
|
||||||
}
|
|
||||||
@@ -1,99 +0,0 @@
|
|||||||
import { atom } from "recoil";
|
|
||||||
import { syncEffect } from "recoil-sync";
|
|
||||||
import TinyColor from "tinycolor2";
|
|
||||||
import { custom, stringLiterals } from "@recoiljs/refine";
|
|
||||||
import { IconStyle } from "@phosphor-icons/core";
|
|
||||||
import { IconEntry } from "@/lib";
|
|
||||||
|
|
||||||
export const searchQueryAtom = atom<string>({
|
|
||||||
key: "searchQuery",
|
|
||||||
default: "",
|
|
||||||
effects: [
|
|
||||||
syncEffect({
|
|
||||||
itemKey: "q",
|
|
||||||
refine: custom((q) => {
|
|
||||||
return (q as string).toString() ?? "";
|
|
||||||
}),
|
|
||||||
syncDefault: false,
|
|
||||||
}),
|
|
||||||
],
|
|
||||||
});
|
|
||||||
|
|
||||||
export const iconWeightAtom = atom<IconStyle>({
|
|
||||||
key: "iconWeight",
|
|
||||||
default: IconStyle.REGULAR,
|
|
||||||
effects: [
|
|
||||||
syncEffect<IconStyle>({
|
|
||||||
itemKey: "weight",
|
|
||||||
refine: stringLiterals({
|
|
||||||
thin: IconStyle.THIN,
|
|
||||||
light: IconStyle.LIGHT,
|
|
||||||
regular: IconStyle.REGULAR,
|
|
||||||
bold: IconStyle.BOLD,
|
|
||||||
fill: IconStyle.FILL,
|
|
||||||
duotone: IconStyle.DUOTONE,
|
|
||||||
}),
|
|
||||||
write: (atom, w) => {
|
|
||||||
if (typeof w === "string") {
|
|
||||||
atom.write("weight", w);
|
|
||||||
} else {
|
|
||||||
atom.reset("weight");
|
|
||||||
}
|
|
||||||
},
|
|
||||||
syncDefault: false,
|
|
||||||
}),
|
|
||||||
],
|
|
||||||
});
|
|
||||||
|
|
||||||
export const iconSizeAtom = atom<number>({
|
|
||||||
key: "iconSize",
|
|
||||||
default: 32,
|
|
||||||
effects: [
|
|
||||||
syncEffect({
|
|
||||||
itemKey: "size",
|
|
||||||
refine: custom((s) => {
|
|
||||||
const size = Number.isFinite(Number(s)) ? Number(s) : 32;
|
|
||||||
return Math.min(Math.max(size, 16), 96);
|
|
||||||
}),
|
|
||||||
syncDefault: false,
|
|
||||||
}),
|
|
||||||
],
|
|
||||||
});
|
|
||||||
|
|
||||||
export const iconColorAtom = atom<string>({
|
|
||||||
key: "iconColor",
|
|
||||||
default: "#000000",
|
|
||||||
effects: [
|
|
||||||
syncEffect({
|
|
||||||
itemKey: "color",
|
|
||||||
refine: custom((c) => {
|
|
||||||
if (typeof c === "string") {
|
|
||||||
const normalizedColor = TinyColor(c);
|
|
||||||
if (normalizedColor.isValid()) {
|
|
||||||
return normalizedColor.toHexString();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return "#000000";
|
|
||||||
}),
|
|
||||||
write: (atom, c) => {
|
|
||||||
if (typeof c === "string") {
|
|
||||||
const color = c.replace("#", "");
|
|
||||||
atom.write("color", color);
|
|
||||||
} else {
|
|
||||||
atom.reset("color");
|
|
||||||
}
|
|
||||||
},
|
|
||||||
syncDefault: false,
|
|
||||||
}),
|
|
||||||
],
|
|
||||||
});
|
|
||||||
|
|
||||||
export const iconPreviewOpenAtom = atom<string | false>({
|
|
||||||
key: "iconPreviewOpen",
|
|
||||||
default: false,
|
|
||||||
});
|
|
||||||
|
|
||||||
export const selectionEntryAtom = atom<IconEntry | null>({
|
|
||||||
key: "selectionEntry",
|
|
||||||
default: null,
|
|
||||||
});
|
|
||||||
@@ -1,4 +1,238 @@
|
|||||||
export * from "./atoms";
|
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;
|
||||||
|
}
|
||||||
|
|||||||
@@ -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);
|
|
||||||
},
|
|
||||||
});
|
|
||||||
@@ -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";
|
||||||
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user