1 Commits

Author SHA1 Message Date
rektdeckard
11b1922f49 fix(app): update vue snippet syntax 2024-06-04 11:04:43 -06:00
43 changed files with 1549 additions and 2049 deletions

View File

@@ -1,6 +0,0 @@
# 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 Normal file
View File

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

View File

@@ -1,15 +0,0 @@
---
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

Binary file not shown.

Before

Width:  |  Height:  |  Size: 16 KiB

View File

@@ -1,53 +0,0 @@
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

View File

@@ -1,55 +0,0 @@
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

View File

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

View File

@@ -1,10 +1,7 @@
<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
@@ -13,7 +10,6 @@ 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
@@ -21,27 +17,18 @@ 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 stylesheet for each weight you need 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 script 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>
<link <script src="https://unpkg.com/@phosphor-icons/web"></script>
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 ph-smiley"></i> <i class="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 ph-cube"></i> <i class="ph-thin ph-cube"></i>
</body> </body>
</html> </html>
``` ```
@@ -102,61 +89,39 @@ ReactDOM.render(<App />, document.getElementById("root"));
> [!NOTE] > [!NOTE]
> Due to possible namespace collisions with built-in HTML elements, compononent names in the Vue library are prefixed with `Ph`, but otherwise follow the same naming conventions. Both Pascal and kebab-case conventions can be used in templates. > Due to possible namespace collisions with built-in HTML elements, compononent names in the Vue library are prefixed with `Ph`, but otherwise follow the same naming conventions. Both Pascal and kebab-case conventions can be used in templates.
<!-- BEGIN_LINKS --> ## Our Related Projects
## Our Projects
- [@phosphor-icons/homepage](https://github.com/phosphor-icons/homepage) ▲ Phosphor homepage and general info - [@phosphor-icons/homepage](https://github.com/phosphor-icons/homepage) ▲ Phosphor homepage and general info
- [@phosphor-icons/core](https://github.com/phosphor-icons/core) ▲ Phosphor icon assets and catalog - [@phosphor-icons/core](https://github.com/phosphor-icons/core) ▲ Phosphor icon assets and catalog
- [@phosphor-icons/elm](https://github.com/phosphor-icons/phosphor-elm) ▲ Phosphor icons for Elm
- [@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/react](https://github.com/phosphor-icons/react) ▲ Phosphor icon component library for React
- [@phosphor-icons/sketch](https://github.com/phosphor-icons/sketch) ▲ Phosphor icons Sketch plugin
- [@phosphor-icons/swift](https://github.com/phosphor-icons/swift) ▲ Phosphor icon component library for SwiftUI
- [@phosphor-icons/theme](https://github.com/phosphor-icons/theme) ▲ A VS Code (and other IDE) theme with the Phosphor color palette
- [@phosphor-icons/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/web](https://github.com/phosphor-icons/web) ▲ Phosphor icons for Vanilla JS
- [@phosphor-icons/vue](https://github.com/phosphor-icons/vue) ▲ Phosphor icon component library for Vue
- [@phosphor-icons/swift](https://github.com/phosphor-icons/swift) ▲ Phosphor icon component library for SwiftUI
- [@phosphor-icons/elm](https://github.com/phosphor-icons/phosphor-elm) ▲ Phosphor icons for Elm
- [@phosphor-icons/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/webcomponents](https://github.com/phosphor-icons/webcomponents) ▲ Phosphor icons as Web Components
- [@phosphor-icons/figma](https://github.com/phosphor-icons/figma) ▲ Phosphor icons Figma plugin
- [@phosphor-icons/sketch](https://github.com/phosphor-icons/sketch) ▲ Phosphor icons Sketch plugin
- [@phosphor-icons/pack](https://github.com/phosphor-icons/pack) ▲ Phosphor web font stripper to generate minimal icon bundles
- [@phosphor-icons/theme](https://github.com/phosphor-icons/theme) ▲ A VS Code (and other IDE) theme with the Phosphor color palette
## Community Projects ## Community Projects
- [adamglin0/compose-phosphor-icons](https://github.com/adamglin0/compose-phosphor-icon) ▲ Phosphor icons for Compose Multiplatform - [phosphor-react-native](https://github.com/duongdev/phosphor-react-native) ▲ Phosphor icon component library for React Native
- [altdsoy/phosphor_icons](https://github.com/altdsoy/phosphor_icons) ▲ Phosphor icons for Phoenix and TailwindCSS - [phosphor-svelte](https://github.com/haruaki07/phosphor-svelte) ▲ Phosphor icons for Svelte apps
- [amPerl/egui-phosphor](https://github.com/amperl/egui-phosphor) ▲ Phosphor icons for egui apps (Rust) - [phosphor-r](https://github.com/dreamRs/phosphoricons) ▲ Phosphor icon wrapper for R documents and applications
- [babakfp/phosphor-icons-svelte](https://github.com/babakfp/phosphor-icons-svelte) ▲ Phosphor icons for Svelte apps - [blade-phosphor-icons](https://github.com/codeat3/blade-phosphor-icons) ▲ Phosphor icons in your Laravel Blade views
- [brettkolodny/phosphor-lustre](https://github.com/brettkolodny/phosphor-lustre) ▲ Phosphor icons for Lustre
- [cellularmitosis/phosphor-uikit](https://github.com/cellularmitosis/phosphor-uikit) ▲ XCode asset catalog generator for Phosphor icons (Swift/UIKit)
- [cjohansen/phosphor-clj](https://github.com/cjohansen/phosphor-clj) ▲ Phosphor icons as Hiccup for Clojure and ClojureScript
- [codeat3/blade-phosphor-icons](https://github.com/codeat3/blade-phosphor-icons) ▲ Phosphor icons in your Laravel Blade views
- [dreamRs/phosphor-r](https://github.com/dreamRs/phosphoricons) ▲ Phosphor icon wrapper for R documents and applications
- [duongdev/phosphor-react-native](https://github.com/duongdev/phosphor-react-native) ▲ Phosphor icon component library for React Native
- [haruaki07/phosphor-svelte](https://github.com/haruaki07/phosphor-svelte) ▲ Phosphor icons for Svelte apps
- [IgnaceMaes/ember-phosphor-icons](https://github.com/IgnaceMaes/ember-phosphor-icons) ▲ Phosphor icons for Ember apps
- [iota-uz/icons](https://github.com/iota-uz/icons) ▲ Phosphor icons as Templ components (Go)
- [jajuma/phosphorhyva](https://github.com/JaJuMa-GmbH/phosphor-hyva) ▲ Phosphor icons for Magento 2 & Mage-OS with Hyvä Theme
- [Kitten](https://kitten.small-web.org/reference/#icons) ▲ Phosphor icons integrated by default in Kitten
- [lucagoslar/phosphor-css](https://github.com/lucagoslar/phosphor-css) ▲ CSS wrapper for Phosphor SVG icons
- [maful/ruby-phosphor-icons](https://github.com/maful/ruby-phosphor-icons) ▲ Phosphor icons for Ruby and Rails applications
- [meadowsys/phosphor-svgs](https://github.com/meadowsys/phosphor-svgs) ▲ Phosphor icons as Rust string constants
- [mwood/tamagui-phosphor-icons](https://github.com/mwood23/tamagui-phosphor-icons) ▲ Phosphor icons for Tamagui
- [noozo/phosphoricons_elixir](https://github.com/noozo/phosphoricons_elixir) ▲ Phosphor icons as SVG strings for Elixir/Phoenix
- [oyedejioyewole/nuxt-phosphor-icons](https://github.com/oyedejioyewole/nuxt-phosphor-icons) ▲ Phosphor icons integration for Nuxt
- [pepaslabs/phosphor-uikit](https://github.com/pepaslabs/phosphor-uikit) ▲ Xcode asset catalog generator for Swift/UIKit
- [raycast/phosphor-icons](https://www.raycast.com/marinsokol/phosphor-icons) ▲ Phosphor icons Raycast extension
- [reatlat/eleventy-plugin-phosphoricons](https://github.com/reatlat/eleventy-plugin-phosphoricons) ▲ An Eleventy shortcode plugin to embed icons as inline SVGs
- [robruiz/wordpress-phosphor-icons-block](https://github.com/robruiz/phosphor-icons-block) ▲ Phosphor icon block for use in WordPress v5.8+
- [sachaw/solid-phosphor](https://github.com/sachaw/solid-phosphor) ▲ Phosphor icons for SolidJS
- [SeanMcP/phosphor-astro](https://github.com/SeanMcP/phosphor-astro) ▲ Phosphor icons as Astro components
- [SorenHolstHansen/phosphor-leptos](https://github.com/SorenHolstHansen/phosphor-leptos) ▲ Phosphor icon component library for Leptos apps (Rust)
- [vnphanquang/phosphor-icons-tailwindcss](https://github.com/vnphanquang/phosphor-icons-tailwindcss) ▲ TailwindCSS plugin for Phosphor icons
- [wireui/phosphoricons](https://github.com/wireui/phosphoricons) ▲ Phosphor icons for Laravel - [wireui/phosphoricons](https://github.com/wireui/phosphoricons) ▲ Phosphor icons for Laravel
- [phosphor-css](https://github.com/lucagoslar/phosphor-css) ▲ CSS wrapper for Phosphor SVG icons
- [ruby-phosphor-icons](https://github.com/maful/ruby-phosphor-icons) ▲ Phosphor icons for Ruby and Rails applications
- [eleventy-plugin-phosphoricons](https://github.com/reatlat/eleventy-plugin-phosphoricons) ▲ An Eleventy plugin for add shortcode, allows Phosphor icons to be embedded as inline svg into templates
- [phosphor-leptos](https://github.com/SorenHolstHansen/phosphor-leptos) ▲ Phosphor icon component library for Leptos apps (rust)
- [wordpress-phosphor-icons-block](https://github.com/robruiz/phosphor-icons-block) ▲ Phosphor icon block for use in WordPress v5.8+
- [ember-phosphor-icons](https://github.com/IgnaceMaes/ember-phosphor-icons) ▲ Phosphor icons for Ember apps
If you've made a port of Phosphor and you want to see it here, just open a PR [here](https://github.com/phosphor-icons/homepage)! If you've made a port of Phosphor and you want to see it here, just open a PR [here](https://github.com/phosphor-icons/homepage)!
## License ## License
MIT © [Phosphor Icons](https://github.com/phosphor-icons) MIT © [Phosphor Icons](https://github.com/phosphor-icons)
<!-- END_LINKS -->

View File

@@ -1,6 +1,6 @@
{ {
"name": "@phosphor-icons/homepage", "name": "@phosphor-icons/homepage",
"version": "2.1.1", "version": "2.0.6",
"license": "MIT", "license": "MIT",
"homepage": "https://phosphoricons.com", "homepage": "https://phosphoricons.com",
"author": { "author": {
@@ -20,18 +20,17 @@
], ],
"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.8", "@phosphor-icons/react": "^2.1.4",
"@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",
@@ -40,9 +39,10 @@
"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",

2019
pnpm-lock.yaml generated

File diff suppressed because it is too large Load Diff

View File

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

View File

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

View File

@@ -1,4 +1,5 @@
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";
@@ -9,13 +10,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 { ApplicationTheme, useApplicationStore } from "@/state"; import { isDarkThemeSelector } 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 = useApplicationStore.use.applicationTheme() === ApplicationTheme.DARK; const isDark = useRecoilValue(isDarkThemeSelector);
useCSSVariables( useCSSVariables(
useMemo( useMemo(

View File

@@ -33,13 +33,13 @@
align-items: center; align-items: center;
justify-content: space-between; justify-content: space-between;
gap: 20px; gap: 20px;
max-width: 800px; max-width: 600px;
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: 16px; border-radius: 32px;
filter: drop-shadow(2px 2px 0 var(--moss-shadow)); filter: drop-shadow(2px 2px 0 var(--moss-shadow));
font-family: "IBM Plex Mono"; font-family: "IBM Plex Mono";
font-size: 14px; font-size: 14px;

View File

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

View File

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

View File

@@ -1,12 +1,13 @@
import { useRecoilValue } from "recoil";
import { motion, AnimatePresence, Variants } from "framer-motion"; import { motion, AnimatePresence, Variants } from "framer-motion";
import { ArrowULeftUpIcon, CoffeeIcon, HandHeartIcon } from "@phosphor-icons/react"; import { ArrowULeftUp, Coffee, HandHeart } 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 { useApplicationStore } from "@/state"; import { selectionEntryAtom } from "@/state";
import "./Footer.css"; import "./Footer.css";
type FooterProps = {}; type FooterProps = {};
@@ -19,7 +20,7 @@ const variants: Variants = {
const Footer = (_: FooterProps) => { const Footer = (_: FooterProps) => {
const isMobile = useMediaQuery("(max-width: 719px)"); const isMobile = useMediaQuery("(max-width: 719px)");
const isViewing = !!useApplicationStore.use.selectionEntry(); const isViewing = !!useRecoilValue(selectionEntryAtom);
return ( return (
<footer> <footer>
@@ -41,7 +42,7 @@ const Footer = (_: FooterProps) => {
?.scrollIntoView({ behavior: "smooth", block: "start" }); ?.scrollIntoView({ behavior: "smooth", block: "start" });
}} }}
> >
<ArrowULeftUpIcon size="1em" /> <ArrowULeftUp size="1em" />
</motion.button> </motion.button>
)} )}
</AnimatePresence> </AnimatePresence>
@@ -64,14 +65,6 @@ 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"
@@ -83,8 +76,8 @@ const Footer = (_: FooterProps) => {
Framer Framer
</a> </a>
,{" "} ,{" "}
<a className="main-link" href="https://www.khanacademy.org/"> <a className="main-link" href="https://www.outgo.co/">
Khan Academy Outgo
</a> </a>
,{" "} ,{" "}
<a <a
@@ -136,7 +129,7 @@ const Footer = (_: FooterProps) => {
) )
} }
> >
<CoffeeIcon size={24} /> <Coffee size={24} />
Buy us a coffee Buy us a coffee
</button> </button>
<button <button
@@ -149,7 +142,7 @@ const Footer = (_: FooterProps) => {
) )
} }
> >
<HandHeartIcon size={24} /> <HandHeart size={24} />
Become a patron Become a patron
</button> </button>
</div> </div>

View File

@@ -1,8 +1,8 @@
import { import {
ArrowCircleUpRightIcon, ArrowCircleUpRight,
ArrowCircleDownIcon, ArrowCircleDown,
MegaphoneSimpleIcon, MegaphoneSimple,
HandHeartIcon, HandHeart,
} from "@phosphor-icons/react"; } from "@phosphor-icons/react";
import Banner from "@/components/Banner"; import Banner from "@/components/Banner";
@@ -40,31 +40,26 @@ const Header = (_: HeaderProps) => {
return ( return (
<header> <header>
<Banner.Container> <Banner.Container>
<Banner id={"rename-notice"}> <Banner id={"2.1.0"}>
<div className="message"> <div className="message">
<MegaphoneSimpleIcon size={32} mirrored /> <MegaphoneSimple size={32} mirrored />
<small> <small>
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{" "} Phosphor v2.1 is out, adding 268 new icons and some general
<a href="https://github.com/phosphor-icons/react/releases/tag/v2.1.8"> revisions. Check our{" "}
<a href="https://github.com/phosphor-icons/homepage/releases">
release notes release notes
</a>{" "} </a>{" "}
for details. to see what's changed!
</small> </small>
</div> </div>
</Banner> </Banner>
<Banner id={"buymeacoffee2"}> <Banner id={"buymeacoffee2"}>
<div className="message"> <div className="message">
<HandHeartIcon size={32} mirrored /> <HandHeart 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!
@@ -88,11 +83,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}>
<ArrowCircleUpRightIcon size={24} /> <ArrowCircleUpRight size={24} />
Get started Get started
</button> </button>
<button className="main-button" onClick={handleScrollToIcons}> <button className="main-button" onClick={handleScrollToIcons}>
<ArrowCircleDownIcon size={24} /> <ArrowCircleDown size={24} />
Explore icons Explore icons
</button> </button>
</div> </div>

View File

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

View File

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

View File

@@ -5,11 +5,11 @@ import {
MutableRefObject, MutableRefObject,
HTMLAttributes, HTMLAttributes,
} from "react"; } from "react";
import { useRecoilState } from "recoil";
import { motion } from "framer-motion"; import { motion } from "framer-motion";
import { useShallow } from "zustand/react/shallow";
import { IconEntry } from "@/lib"; import { IconEntry } from "@/lib";
import { useApplicationStore } from "@/state"; import { selectionEntryAtom } from "@/state";
interface IconGridItemProps extends HTMLAttributes<HTMLDivElement> { interface IconGridItemProps extends HTMLAttributes<HTMLDivElement> {
index: number; index: number;
@@ -33,10 +33,7 @@ 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 } = useApplicationStore(useShallow((state) => ({ const [selection, setSelectionEntry] = useRecoilState(selectionEntryAtom);
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*");
@@ -71,10 +68,11 @@ const IconGridItem = (props: IconGridItemProps) => {
}, [originOffset]); }, [originOffset]);
return ( return (
<motion.button <motion.div
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,
@@ -82,6 +80,7 @@ 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 />
@@ -90,7 +89,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.button> </motion.div>
); );
}; };

View File

@@ -5,17 +5,18 @@ 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 {
CopyIcon, Copy,
CheckCircleIcon, CheckCircle,
ArrowFatLinesDownIcon, ArrowFatLinesDown,
XCircleIcon, XCircle,
CaretDoubleLeftIcon, CaretDoubleLeft,
CaretDoubleRightIcon, CaretDoubleRight,
} 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";
@@ -23,7 +24,13 @@ 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 { useApplicationStore } from "@/state"; import {
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";
@@ -75,7 +82,7 @@ const ActionButton = (
} & HTMLAttributes<HTMLButtonElement> } & HTMLAttributes<HTMLButtonElement>
) => { ) => {
const { active, download, label, ...rest } = props; const { active, download, label, ...rest } = props;
const Icon = download ? ArrowFatLinesDownIcon : CopyIcon; const Icon = download ? ArrowFatLinesDown : Copy;
return ( return (
<button <button
{...rest} {...rest}
@@ -84,7 +91,7 @@ const ActionButton = (
tabIndex={0} tabIndex={0}
> >
{active ? ( {active ? (
<CheckCircleIcon size={20} color="var(--olive)" weight="fill" /> <CheckCircle size={20} color="var(--olive)" weight="fill" />
) : ( ) : (
<Icon size={20} color="currentColor" weight="fill" /> <Icon size={20} color="currentColor" weight="fill" />
)} )}
@@ -94,14 +101,12 @@ const ActionButton = (
}; };
const Panel = () => { const Panel = () => {
const { const [entry, setSelectionEntry] = useRecoilState(selectionEntryAtom);
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
@@ -164,13 +169,13 @@ const Panel = () => {
onClick={(e) => handleCopySnippet(e, type)} onClick={(e) => handleCopySnippet(e, type)}
> >
{copied === type ? ( {copied === type ? (
<CheckCircleIcon <CheckCircle
size={20} size={20}
color="var(--olive)" color="var(--olive)"
weight="fill" weight="fill"
/> />
) : ( ) : (
<CopyIcon size={20} color="var(--foreground)" weight="fill" /> <Copy size={20} color="var(--foreground)" weight="fill" />
)} )}
</button> </button>
)} )}
@@ -182,7 +187,7 @@ const Panel = () => {
); );
return [snippets, tabs]; return [snippets, tabs];
}, [entry, weight, size, color, copied]); }, [entry, weight, size, color, copied, isDark]);
useHotkeys("esc", () => setSelectionEntry(null)); useHotkeys("esc", () => setSelectionEntry(null));
@@ -241,7 +246,8 @@ 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}${weight === "regular" ? "" : `-${weight}` `https://raw.githubusercontent.com/phosphor-icons/core/main/raw/${weight}/${name}${
weight === "regular" ? "" : `-${weight}`
}.svg` }.svg`
); );
const content = await data.text(); const content = await data.text();
@@ -276,7 +282,8 @@ const Panel = () => {
const { name } = entry; const { name } = entry;
saveAs( saveAs(
`https://raw.githubusercontent.com/phosphor-icons/core/main/raw/${weight}/${name}${weight === "regular" ? "" : `-${weight}` `https://raw.githubusercontent.com/phosphor-icons/core/main/raw/${weight}/${name}${
weight === "regular" ? "" : `-${weight}`
}.svg`, }.svg`,
`${entry?.name}${weight === "regular" ? "" : `-${weight}`}.svg` `${entry?.name}${weight === "regular" ? "" : `-${weight}`}.svg`
); );
@@ -426,13 +433,13 @@ const Panel = () => {
onClick={() => setShowMoreActions((s) => !s)} onClick={() => setShowMoreActions((s) => !s)}
> >
{!showMoreActions ? ( {!showMoreActions ? (
<CaretDoubleRightIcon <CaretDoubleRight
size={16} size={16}
weight="bold" weight="bold"
color="var(--foreground)" color="var(--foreground)"
/> />
) : ( ) : (
<CaretDoubleLeftIcon <CaretDoubleLeft
size={16} size={16}
weight="bold" weight="bold"
color="var(--foreground)" color="var(--foreground)"
@@ -452,7 +459,7 @@ const Panel = () => {
e.key === "Enter" && setSelectionEntry(null); e.key === "Enter" && setSelectionEntry(null);
}} }}
> >
<XCircleIcon color="currentColor" size={28} weight="fill" /> <XCircle color="currentColor" size={28} weight="fill" />
</button> </button>
</motion.aside> </motion.aside>
)} )}

View File

@@ -1,7 +1,8 @@
import { useCallback } from "react"; import { useCallback } from "react";
import { useSetRecoilState } from "recoil";
import { useMediaQuery } from "@/hooks"; import { useMediaQuery } from "@/hooks";
import { useApplicationStore } from "@/state"; import { searchQueryAtom } from "@/state";
import "./TagCloud.css"; import "./TagCloud.css";
interface TagCloudProps { interface TagCloudProps {
@@ -11,7 +12,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 = useApplicationStore.use.setSearchQuery(); const setQuery = useSetRecoilState(searchQueryAtom);
const handleTagClick = useCallback( const handleTagClick = useCallback(
(tag: string) => { (tag: string) => {
setQuery(tag); setQuery(tag);

View File

@@ -1,4 +1,4 @@
import { ArrowElbowDownRightIcon } from "@phosphor-icons/react"; import { ArrowElbowDownRight } from "@phosphor-icons/react";
import { iconCount } from "@/lib/icons"; import { iconCount } from "@/lib/icons";
import OutboundLink from "@/components/OutboundLink"; import OutboundLink from "@/components/OutboundLink";
@@ -11,10 +11,10 @@ const Links = (_: LinksProps) => {
return ( return (
<div className="links"> <div className="links">
<div> <div>
<ArrowElbowDownRightIcon size={24} /> <ArrowElbowDownRight size={24} />
<OutboundLink <OutboundLink
className="nav-link" className="nav-link"
href="/assets/phosphor-icons.zip" href="https://phosphoricons.com/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>
<ArrowElbowDownRightIcon size={24} /> <ArrowElbowDownRight 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>
<ArrowElbowDownRightIcon size={24} /> <ArrowElbowDownRight size={24} />
<OutboundLink <OutboundLink
href="/assets/phosphor-icons.sketchplugin.zip" href="https://phosphoricons.com/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>
<ArrowElbowDownRightIcon size={24} /> <ArrowElbowDownRight 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>
<ArrowElbowDownRightIcon size={24} /> <ArrowElbowDownRight 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>
<ArrowElbowDownRightIcon size={24} /> <ArrowElbowDownRight 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>
<ArrowElbowDownRightIcon size={24} /> <ArrowElbowDownRight 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>
<ArrowElbowDownRightIcon size={24} /> <ArrowElbowDownRight size={24} />
<OutboundLink <OutboundLink
href="https://twitter.com/_phosphoricons" href="https://twitter.com/_phosphoricons"
eventLabel="Twitter" eventLabel="Twitter"

View File

@@ -1,6 +1,6 @@
import { ReactNode } from "react"; import { ReactNode } from "react";
import { motion } from "framer-motion"; import { motion } from "framer-motion";
import { HourglassMediumIcon, QuestionIcon, SmileyXEyesIcon } from "@phosphor-icons/react"; import { HourglassMedium, Question, SmileyXEyes } 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" && <HourglassMediumIcon size={128} weight="fill" />} {type === "wait" && <HourglassMedium size={128} weight="fill" />}
{type === "help" && <QuestionIcon size={128} weight="fill" />} {type === "help" && <Question size={128} weight="fill" />}
{type === "warn" && <SmileyXEyesIcon size={128} weight="fill" />} {type === "warn" && <SmileyXEyes size={128} weight="fill" />}
<p>{message}</p> <p>{message}</p>
{children} {children}
</div> </div>

View File

@@ -1,4 +1,4 @@
import { ArrowCircleUpRightIcon } from "@phosphor-icons/react"; import { ArrowCircleUpRight } 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>
<ArrowCircleUpRightIcon weight="fill" size={32} /> <ArrowCircleUpRight weight="fill" size={32} />
</div> </div>
<Example /> <Example />
</a> </a>

View File

@@ -2,10 +2,10 @@ import { useMemo } from "react";
import { import {
Icon, Icon,
IconProps, IconProps,
BarricadeIcon, Barricade,
GasCanIcon, GasCan,
IceCreamIcon, IceCream,
FlyingSaucerIcon, FlyingSaucer,
} 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={FlyingSaucerIcon} duocolor="darkcyan" /> <Duocolor Icon={FlyingSaucer} duocolor="darkcyan" />
<Duocolor Icon={BarricadeIcon} color="darkgray" duocolor="orange" /> <Duocolor Icon={Barricade} color="darkgray" duocolor="orange" />
<Duocolor Icon={IceCreamIcon} color="saddlebrown" duocolor="lightpink" /> <Duocolor Icon={IceCream} color="saddlebrown" duocolor="lightpink" />
<Duocolor Icon={GasCanIcon} duocolor="indianred" /> <Duocolor Icon={GasCan} duocolor="indianred" />
</div> </div>
); );
}, },

View File

@@ -1,4 +1,4 @@
import { FireIcon, ImageIcon, PeaceIcon, RainbowCloudIcon } from "@phosphor-icons/react"; import { Fire, Image, Peace, RainbowCloud } 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">
<FireIcon weight="fill" color="url(#flame)"> <Fire 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>
</FireIcon> </Fire>
<RainbowCloudIcon color="url(#spectrum)"> <RainbowCloud 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>
</RainbowCloudIcon> </RainbowCloud>
<PeaceIcon weight="fill" color="url(#spectrum2)"> <Peace 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>
</PeaceIcon> </Peace>
<ImageIcon color="url(#sunset)" weight="fill"> <Image 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>
</ImageIcon> </Image>
</div> </div>
); );
}, },

View File

@@ -1,4 +1,4 @@
import { CassetteTapeIcon, CubeIcon, VirusIcon, ThumbsUpIcon } from "@phosphor-icons/react"; import { CassetteTape, Cube, Virus, ThumbsUp } 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">
<CassetteTapeIcon <CassetteTape
color="teal" color="teal"
style={{ filter: "url(#displacementFilter)" }} style={{ filter: "url(#displacementFilter)" }}
> >
@@ -29,10 +29,10 @@ const animation: RecipeProps = {
/> />
</filter> </filter>
</defs> </defs>
</CassetteTapeIcon> </CassetteTape>
<CubeIcon color="teal" style={{ filter: "url(#displacementFilter)" }} /> <Cube color="teal" style={{ filter: "url(#displacementFilter)" }} />
<ThumbsUpIcon color="teal" style={{ filter: "url(#displacementFilter)" }} /> <ThumbsUp color="teal" style={{ filter: "url(#displacementFilter)" }} />
<VirusIcon color="teal" style={{ filter: "url(#displacementFilter)" }} /> <Virus color="teal" style={{ filter: "url(#displacementFilter)" }} />
</div> </div>
); );
}, },

View File

@@ -5,19 +5,19 @@ import {
MutableRefObject, MutableRefObject,
ReactNode, ReactNode,
} from "react"; } from "react";
import { useShallow } from "zustand/react/shallow"; import { useRecoilState } from "recoil";
import { useHotkeys } from "react-hotkeys-hook"; import { useHotkeys } from "react-hotkeys-hook";
import { import {
CommandIcon, Command,
MagnifyingGlassIcon, MagnifyingGlass,
XIcon, X,
HourglassHighIcon, HourglassHigh,
} 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,10 +29,7 @@ type SearchInputProps = {};
const SearchInput = (_: SearchInputProps) => { const SearchInput = (_: SearchInputProps) => {
const [value, setValue] = useState<string>(""); const [value, setValue] = useState<string>("");
const { query, setQuery } = useApplicationStore(useShallow((state) => ({ const [query, setQuery] = useRecoilState(searchQueryAtom);
query: state.searchQuery,
setQuery: state.setSearchQuery,
})));
const inputRef = const inputRef =
useRef<HTMLInputElement>() as MutableRefObject<HTMLInputElement>; useRef<HTMLInputElement>() as MutableRefObject<HTMLInputElement>;
@@ -80,7 +77,7 @@ const SearchInput = (_: SearchInputProps) => {
return ( return (
<div className="search-bar"> <div className="search-bar">
<MagnifyingGlassIcon id="search-icon" size={24} /> <MagnifyingGlass id="search-icon" size={24} />
<input <input
ref={inputRef} ref={inputRef}
id="search-input" id="search-input"
@@ -96,12 +93,12 @@ const SearchInput = (_: SearchInputProps) => {
(key === "Enter" || key === "Escape") && currentTarget.blur() (key === "Enter" || key === "Escape") && currentTarget.blur()
} }
/> />
{!value && !isMobile && <Keys>{isApple ? <CommandIcon /> : "Ctrl + "}K</Keys>} {!value && !isMobile && <Keys>{isApple ? <Command /> : "Ctrl + "}K</Keys>}
{value ? ( {value ? (
value === query ? ( value === query ? (
<XIcon className="clear-icon" size={18} onClick={handleCancelSearch} /> <X className="clear-icon" size={18} onClick={handleCancelSearch} />
) : ( ) : (
<HourglassHighIcon className="wait-icon" weight="fill" size={18} /> <HourglassHigh className="wait-icon" weight="fill" size={18} />
) )
) : null} ) : null}
</div> </div>

View File

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

View File

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

View File

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

View File

@@ -3,7 +3,7 @@ export { default as useDebounce } from "./useDebounce";
export { default as useEvent } from "./useEvent"; export { default as useEvent } from "./useEvent";
export { default as useLocalStorage } from "./useLocalStorage"; export { default as useLocalStorage } from "./useLocalStorage";
export { default as useMediaQuery } from "./useMediaQuery"; export { default as useMediaQuery } from "./useMediaQuery";
// export { default as usePersistSettings } from "./usePersistSettings"; export { default as usePersistSettings } from "./usePersistSettings";
export { default as useSessionStorage } from "./useSessionStorage"; export { default as useSessionStorage } from "./useSessionStorage";
export { default as useThrottle } from "./useThrottle"; export { default as useThrottle } from "./useThrottle";
export { default as useThrottled } from "./useThrottled"; export { default as useThrottled } from "./useThrottled";

View File

@@ -0,0 +1,23 @@
import { useRecoilValue } from "recoil";
import {
iconWeightAtom,
iconSizeAtom,
iconColorAtom,
STORAGE_KEY,
} from "@/state";
import useDebounce from "./useDebounce";
export default function usePersistSettings() {
const weight = useRecoilValue(iconWeightAtom);
const size = useRecoilValue(iconSizeAtom);
const color = useRecoilValue(iconColorAtom);
useDebounce(
() => {
const serializedState = JSON.stringify({ weight, size, color });
window.localStorage.setItem(STORAGE_KEY, serializedState);
},
2000,
[weight, size, color]
);
}

View File

@@ -1,5 +1,7 @@
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";
@@ -13,6 +15,7 @@ const root = createRoot(container!);
root.render( root.render(
<StrictMode> <StrictMode>
<RecoilRoot>
<ErrorBoundary <ErrorBoundary
fallback={ fallback={
<Notice <Notice
@@ -25,8 +28,11 @@ root.render(
/> />
} }
> >
<RecoilURLSyncJSON location={{ part: "queryParams" }}>
<App /> <App />
</RecoilURLSyncJSON>
</ErrorBoundary> </ErrorBoundary>
</RecoilRoot>
</StrictMode> </StrictMode>
); );

View File

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

View File

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

99
src/state/atoms.ts Normal file
View File

@@ -0,0 +1,99 @@
import { atom } from "recoil";
import { syncEffect } from "recoil-sync";
import TinyColor from "tinycolor2";
import { custom, stringLiterals } from "@recoiljs/refine";
import { IconStyle } from "@phosphor-icons/core";
import { IconEntry } from "@/lib";
export const searchQueryAtom = atom<string>({
key: "searchQuery",
default: "",
effects: [
syncEffect({
itemKey: "q",
refine: custom((q) => {
return (q as string).toString() ?? "";
}),
syncDefault: false,
}),
],
});
export const iconWeightAtom = atom<IconStyle>({
key: "iconWeight",
default: IconStyle.REGULAR,
effects: [
syncEffect<IconStyle>({
itemKey: "weight",
refine: stringLiterals({
thin: IconStyle.THIN,
light: IconStyle.LIGHT,
regular: IconStyle.REGULAR,
bold: IconStyle.BOLD,
fill: IconStyle.FILL,
duotone: IconStyle.DUOTONE,
}),
write: (atom, w) => {
if (typeof w === "string") {
atom.write("weight", w);
} else {
atom.reset("weight");
}
},
syncDefault: false,
}),
],
});
export const iconSizeAtom = atom<number>({
key: "iconSize",
default: 32,
effects: [
syncEffect({
itemKey: "size",
refine: custom((s) => {
const size = Number.isFinite(Number(s)) ? Number(s) : 32;
return Math.min(Math.max(size, 16), 96);
}),
syncDefault: false,
}),
],
});
export const iconColorAtom = atom<string>({
key: "iconColor",
default: "#000000",
effects: [
syncEffect({
itemKey: "color",
refine: custom((c) => {
if (typeof c === "string") {
const normalizedColor = TinyColor(c);
if (normalizedColor.isValid()) {
return normalizedColor.toHexString();
}
}
return "#000000";
}),
write: (atom, c) => {
if (typeof c === "string") {
const color = c.replace("#", "");
atom.write("color", color);
} else {
atom.reset("color");
}
},
syncDefault: false,
}),
],
});
export const iconPreviewOpenAtom = atom<string | false>({
key: "iconPreviewOpen",
default: false,
});
export const selectionEntryAtom = atom<IconEntry | null>({
key: "selectionEntry",
default: null,
});

View File

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

88
src/state/selectors.ts Normal file
View File

@@ -0,0 +1,88 @@
import { selector, selectorFamily } from "recoil";
import TinyColor from "tinycolor2";
// @ts-ignore
import Fuse from "fuse.js";
import { IconCategory } from "@phosphor-icons/core";
import {
searchQueryAtom,
iconWeightAtom,
iconSizeAtom,
iconColorAtom,
} from "./atoms";
import { IconEntry } from "@/lib";
import { icons } from "@/lib/icons";
const fuse = new Fuse(icons, {
keys: [{ name: "name", weight: 4 }, "tags", "categories"],
threshold: 0.2, // Tweak this to what feels like the right number of results
// shouldSort: false,
useExtendedSearch: true,
});
export const filteredQueryResultsSelector = selector<ReadonlyArray<IconEntry>>({
key: "filteredQueryResults",
get: ({ get }) => {
const query = get(searchQueryAtom).trim().toLowerCase();
if (!query) return icons;
return new Promise((resolve) =>
// @ts-ignore
resolve(fuse.search(query).map((value) => value.item))
);
},
});
type CategorizedIcons = Partial<Record<IconCategory, IconEntry[]>>;
export const categorizedQueryResultsSelector = selector<
Readonly<CategorizedIcons>
>({
key: "categorizedQueryResults",
get: ({ get }) => {
const filteredResults = get(filteredQueryResultsSelector);
return new Promise((resolve) =>
resolve(
filteredResults.reduce<CategorizedIcons>((acc, curr) => {
curr.categories.forEach((category) => {
if (!acc[category]) acc[category] = [];
acc[category]!!.push(curr);
});
return acc;
}, {})
)
);
},
});
export const singleCategoryQueryResultsSelector = selectorFamily<
ReadonlyArray<IconEntry>,
IconCategory
>({
key: "singleCategoryQueryResults",
get:
(category: IconCategory) =>
({ get }) => {
const filteredResults = get(filteredQueryResultsSelector);
return new Promise((resolve) =>
resolve(
filteredResults.filter((icon) => icon.categories.includes(category))
)
);
},
});
export const isDarkThemeSelector = selector<boolean>({
key: "isDarkTheme",
get: ({ get }) => TinyColor(get(iconColorAtom)).isLight(),
});
export const resetSettingsSelector = selector<null>({
key: "resetSettings",
get: () => null,
set: ({ reset }) => {
reset(iconWeightAtom);
reset(iconSizeAtom);
reset(iconColorAtom);
},
});

View File

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