-
+
- {type === "wait" &&
}
- {type === "help" &&
}
- {type === "warn" &&
}
+ {type === "wait" &&
}
+ {type === "help" &&
}
+ {type === "warn" &&
}
{message}
{children}
diff --git a/src/components/Recipes/Recipe.tsx b/src/components/Recipes/Recipe.tsx
index 510d8b5..4c67036 100644
--- a/src/components/Recipes/Recipe.tsx
+++ b/src/components/Recipes/Recipe.tsx
@@ -1,4 +1,4 @@
-import { ArrowCircleUpRight } from "@phosphor-icons/react";
+import { ArrowCircleUpRightIcon } from "@phosphor-icons/react";
export type RecipeProps = {
title: string;
@@ -12,7 +12,7 @@ const Recipe = ({ url, Example }: RecipeProps) => {
{/* {title}
*/}
diff --git a/src/components/Recipes/items/Duocolor.tsx b/src/components/Recipes/items/Duocolor.tsx
index 3dbd7cf..8dc1345 100644
--- a/src/components/Recipes/items/Duocolor.tsx
+++ b/src/components/Recipes/items/Duocolor.tsx
@@ -2,10 +2,10 @@ import { useMemo } from "react";
import {
Icon,
IconProps,
- Barricade,
- GasCan,
- IceCream,
- FlyingSaucer,
+ BarricadeIcon,
+ GasCanIcon,
+ IceCreamIcon,
+ FlyingSaucerIcon,
} from "@phosphor-icons/react";
import { RecipeProps } from "../Recipe";
@@ -51,10 +51,10 @@ const duocolor: RecipeProps = {
Example() {
return (
-
-
-
-
+
+
+
+
);
},
diff --git a/src/components/Recipes/items/Gradient.tsx b/src/components/Recipes/items/Gradient.tsx
index 6b3a57e..635a6b0 100644
--- a/src/components/Recipes/items/Gradient.tsx
+++ b/src/components/Recipes/items/Gradient.tsx
@@ -1,4 +1,4 @@
-import { Fire, Image, Peace, RainbowCloud } from "@phosphor-icons/react";
+import { FireIcon, ImageIcon, PeaceIcon, RainbowCloudIcon } from "@phosphor-icons/react";
import { RecipeProps } from "../Recipe";
@@ -8,7 +8,7 @@ const gradient: RecipeProps = {
Example() {
return (
-
+
@@ -18,9 +18,9 @@ const gradient: RecipeProps = {
-
+
-
+
@@ -30,9 +30,9 @@ const gradient: RecipeProps = {
-
+
-
+
@@ -42,16 +42,16 @@ const gradient: RecipeProps = {
-
+
-
+
-
+
);
},
diff --git a/src/components/Recipes/items/HandDrawn.tsx b/src/components/Recipes/items/HandDrawn.tsx
index 8311215..2ca828a 100644
--- a/src/components/Recipes/items/HandDrawn.tsx
+++ b/src/components/Recipes/items/HandDrawn.tsx
@@ -1,4 +1,4 @@
-import { CassetteTape, Cube, Virus, ThumbsUp } from "@phosphor-icons/react";
+import { CassetteTapeIcon, CubeIcon, VirusIcon, ThumbsUpIcon } from "@phosphor-icons/react";
import { RecipeProps } from "../Recipe";
@@ -8,7 +8,7 @@ const animation: RecipeProps = {
Example() {
return (
-
@@ -29,10 +29,10 @@ const animation: RecipeProps = {
/>
-
-
-
-
+
+
+
+
);
},
diff --git a/src/components/SearchInput/SearchInput.tsx b/src/components/SearchInput/SearchInput.tsx
index a7b7fe4..d0bf314 100644
--- a/src/components/SearchInput/SearchInput.tsx
+++ b/src/components/SearchInput/SearchInput.tsx
@@ -5,19 +5,19 @@ import {
MutableRefObject,
ReactNode,
} from "react";
-import { useRecoilState } from "recoil";
+import { useShallow } from "zustand/react/shallow";
import { useHotkeys } from "react-hotkeys-hook";
import {
- Command,
- MagnifyingGlass,
- X,
- HourglassHigh,
+ CommandIcon,
+ MagnifyingGlassIcon,
+ XIcon,
+ HourglassHighIcon,
} from "@phosphor-icons/react";
import ReactGA from "react-ga4";
import { useDebounce } from "@/hooks";
-import { searchQueryAtom } from "@/state";
import "./SearchInput.css";
+import { useApplicationStore } from "@/state";
const apple = /iPhone|iPod|iPad|Macintosh|MacIntel|MacPPC/i;
const isApple = apple.test(window.navigator.platform);
@@ -29,7 +29,10 @@ type SearchInputProps = {};
const SearchInput = (_: SearchInputProps) => {
const [value, setValue] = useState("");
- const [query, setQuery] = useRecoilState(searchQueryAtom);
+ const { query, setQuery } = useApplicationStore(useShallow((state) => ({
+ query: state.searchQuery,
+ setQuery: state.setSearchQuery,
+ })));
const inputRef =
useRef() as MutableRefObject;
@@ -77,7 +80,7 @@ const SearchInput = (_: SearchInputProps) => {
return (
-
+
{
(key === "Enter" || key === "Escape") && currentTarget.blur()
}
/>
- {!value && !isMobile && {isApple ? : "Ctrl + "}K}
+ {!value && !isMobile && {isApple ? : "Ctrl + "}K}
{value ? (
value === query ? (
-
+
) : (
-
+
)
) : null}
diff --git a/src/components/SettingsActions/SettingsActions.tsx b/src/components/SettingsActions/SettingsActions.tsx
index 9a78930..6c67494 100644
--- a/src/components/SettingsActions/SettingsActions.tsx
+++ b/src/components/SettingsActions/SettingsActions.tsx
@@ -1,29 +1,27 @@
import { useState } from "react";
import ReactGA from "react-ga4";
-import { useRecoilState, useResetRecoilState, useSetRecoilState } from "recoil";
+import { useShallow } from "zustand/react/shallow";
import {
- ArrowCounterClockwise,
- CheckCircle,
- DiceFive,
- Link,
+ ArrowCounterClockwiseIcon,
+ CheckCircleIcon,
+ DiceFiveIcon,
+ LinkIcon,
} from "@phosphor-icons/react";
import { IconStyle } from "@phosphor-icons/core";
import { useTransientState } from "@/hooks";
-import {
- iconWeightAtom,
- iconSizeAtom,
- iconColorAtom,
- resetSettingsSelector,
-} from "@/state";
+import { useApplicationStore } from "@/state";
import "./SettingsActions.css";
const SettingsActions = () => {
- const [weight, setWeight] = useRecoilState(iconWeightAtom);
- const setSize = useSetRecoilState(iconSizeAtom);
- const setColor = useSetRecoilState(iconColorAtom);
- const reset = useResetRecoilState(resetSettingsSelector);
+ const { weight, setWeight, setSize, setColor, reset } = useApplicationStore(useShallow((state) => ({
+ weight: state.iconWeight,
+ setWeight: state.setIconWeight,
+ setSize: state.setIconSize,
+ setColor: state.setIconColor,
+ reset: state.resetApplicationState,
+ })));
const [copied, setCopied] = useTransientState(false, 2000);
const [booped, setBooped] = useState(false);
@@ -67,7 +65,7 @@ const SettingsActions = () => {
title="Restore default settings"
onClick={reset}
>
-
+
>
diff --git a/src/components/SizeInput/SizeInput.tsx b/src/components/SizeInput/SizeInput.tsx
index 7d8aaf9..d72777e 100644
--- a/src/components/SizeInput/SizeInput.tsx
+++ b/src/components/SizeInput/SizeInput.tsx
@@ -1,7 +1,7 @@
import React, { useCallback } from "react";
-import { useRecoilState } from "recoil";
+import { useShallow } from "zustand/react/shallow";
-import { iconSizeAtom } from "@/state";
+import { useApplicationStore } from "@/state";
import "./SizeInput.css";
type SizeInputProps = {};
@@ -15,7 +15,10 @@ const handleBlur = (event: React.UIEvent) => {
};
const SizeInput = (_: SizeInputProps) => {
- const [size, setSize] = useRecoilState(iconSizeAtom);
+ const { size, setSize } = useApplicationStore(useShallow((state) => ({
+ size: state.iconSize,
+ setSize: state.setIconSize,
+ })));
const handleSizeChange = useCallback(
(event: React.ChangeEvent) => {
diff --git a/src/components/StyleInput/StyleInput.tsx b/src/components/StyleInput/StyleInput.tsx
index f5571ec..c75f261 100644
--- a/src/components/StyleInput/StyleInput.tsx
+++ b/src/components/StyleInput/StyleInput.tsx
@@ -1,10 +1,9 @@
-import { useMemo } from "react";
-import { useRecoilState } from "recoil";
+import { useShallow } from "zustand/react/shallow";
import Select from "react-dropdown-select";
-import { PencilSimpleLine } from "@phosphor-icons/react";
+import { PencilSimpleLineIcon } from "@phosphor-icons/react";
import { IconStyle } from "@phosphor-icons/core";
-import { iconWeightAtom } from "@/state";
+import { useApplicationStore } from "@/state";
import "./StyleInput.css";
@@ -14,44 +13,45 @@ const options: WeightOption[] = [
{
key: "Thin",
value: IconStyle.THIN,
- icon: ,
+ icon: ,
},
{
key: "Light",
value: IconStyle.LIGHT,
- icon: ,
+ icon: ,
},
{
key: "Regular",
value: IconStyle.REGULAR,
- icon: ,
+ icon: ,
},
{
key: "Bold",
value: IconStyle.BOLD,
- icon: ,
+ icon: ,
},
{
key: "Fill",
value: IconStyle.FILL,
- icon: ,
+ icon: ,
},
{
key: "Duotone",
value: IconStyle.DUOTONE,
- icon: ,
+ icon: ,
},
];
type StyleInputProps = {};
-const StyleInput = (_: StyleInputProps) => {
- const [style, setStyle] = useRecoilState(iconWeightAtom);
- const currentStyle = useMemo(
- () => [options.find((option) => option.value === style)!!],
- [style]
- );
+const StyleInput = (_: StyleInputProps) => {
+ const { style, setStyle } = useApplicationStore(useShallow((state) => ({
+ style: state.iconWeight,
+ setStyle: state.setIconWeight,
+ })));
+
+ const currentStyle = [options.find((option) => option.value === style)!];
const handleStyleChange = (values: WeightOption[]) =>
setStyle(values[0].value as IconStyle);
@@ -72,9 +72,8 @@ const StyleInput = (_: StyleInputProps) => {
methods.addItem(item)}
>
{item.icon}
diff --git a/src/hooks/index.ts b/src/hooks/index.ts
index a8ff9b8..00fe5ce 100644
--- a/src/hooks/index.ts
+++ b/src/hooks/index.ts
@@ -3,7 +3,7 @@ export { default as useDebounce } from "./useDebounce";
export { default as useEvent } from "./useEvent";
export { default as useLocalStorage } from "./useLocalStorage";
export { default as useMediaQuery } from "./useMediaQuery";
-export { default as usePersistSettings } from "./usePersistSettings";
+// export { default as usePersistSettings } from "./usePersistSettings";
export { default as useSessionStorage } from "./useSessionStorage";
export { default as useThrottle } from "./useThrottle";
export { default as useThrottled } from "./useThrottled";
diff --git a/src/hooks/usePersistSettings.ts b/src/hooks/usePersistSettings.ts
deleted file mode 100644
index 13a6562..0000000
--- a/src/hooks/usePersistSettings.ts
+++ /dev/null
@@ -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]
- );
-}
diff --git a/src/index.tsx b/src/index.tsx
index 0994f81..22d4e28 100644
--- a/src/index.tsx
+++ b/src/index.tsx
@@ -1,7 +1,5 @@
import { StrictMode } from "react";
import { createRoot } from "react-dom/client";
-import { RecoilRoot } from "recoil";
-import { RecoilURLSyncJSON } from "recoil-sync";
import App from "./components/App";
import ReactGA from "react-ga4";
import ErrorBoundary from "./components/ErrorBoundary";
@@ -15,24 +13,20 @@ const root = createRoot(container!);
root.render(
-
-
- An error occurred. Try going{" "}
- home.
-
- }
- />
- }
- >
-
-
-
-
-
+
+ An error occurred. Try going{" "}
+ home.
+
+ }
+ />
+ }
+ >
+
+
);
diff --git a/src/lib/index.ts b/src/lib/index.ts
index d3a8a29..9e304a2 100644
--- a/src/lib/index.ts
+++ b/src/lib/index.ts
@@ -1,5 +1,6 @@
import { Icon } from "@phosphor-icons/react";
import { IconEntry as CoreEntry } from "@phosphor-icons/core";
+export * from "./icons";
export interface IconEntry extends CoreEntry {
Icon: Icon;
diff --git a/src/state/RecoilSyncLocalStorage.tsx b/src/state/RecoilSyncLocalStorage.tsx
deleted file mode 100644
index af35c97..0000000
--- a/src/state/RecoilSyncLocalStorage.tsx
+++ /dev/null
@@ -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 (
-
- {children}
-
- );
-};
-
-function parseJSON(value: string): unknown {
- return value === "undefined" ? undefined : JSON.parse(value);
-}
diff --git a/src/state/atoms.ts b/src/state/atoms.ts
deleted file mode 100644
index 15ce9e8..0000000
--- a/src/state/atoms.ts
+++ /dev/null
@@ -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({
- key: "searchQuery",
- default: "",
- effects: [
- syncEffect({
- itemKey: "q",
- refine: custom((q) => {
- return (q as string).toString() ?? "";
- }),
- syncDefault: false,
- }),
- ],
-});
-
-export const iconWeightAtom = atom({
- key: "iconWeight",
- default: IconStyle.REGULAR,
- effects: [
- syncEffect({
- 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({
- 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({
- 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({
- key: "iconPreviewOpen",
- default: false,
-});
-
-export const selectionEntryAtom = atom({
- key: "selectionEntry",
- default: null,
-});
diff --git a/src/state/index.ts b/src/state/index.ts
index f9c6a4c..df42120 100644
--- a/src/state/index.ts
+++ b/src/state/index.ts
@@ -1,4 +1,238 @@
-export * from "./atoms";
-export * from "./selectors";
+import Fuse from "fuse.js";
+import { create, type UseBoundStore, type StoreApi } from "zustand";
+import { persist, PersistStorage } from "zustand/middleware";
+
+import TinyColor from "tinycolor2";
+import { IconStyle } from "@phosphor-icons/core";
+import { type IconEntry, icons } from "@/lib";
+import { parseColor, parseQuery, parseSize, parseWeight } from "@/utils";
export const STORAGE_KEY = "__phosphor_settings__";
+
+interface ApplicationFields {
+ // Fields
+ applicationTheme: ApplicationTheme;
+ searchQuery: string;
+ iconWeight: IconStyle;
+ iconSize: number;
+ iconColor: string;
+ iconPreviewOpen: string | false;
+ selectionEntry: IconEntry | null;
+ filteredQueryResults: ReadonlyArray;
+}
+
+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 = {
+ 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()(
+ 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 extends { getState: () => infer T }
+ ? S & { use: { [K in keyof T]: () => T[K] } }
+ : never;
+
+function createSelectors>>(_store: S) {
+ const store = _store as WithSelectors;
+ 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;
+}
diff --git a/src/state/selectors.ts b/src/state/selectors.ts
deleted file mode 100644
index 4f6d856..0000000
--- a/src/state/selectors.ts
+++ /dev/null
@@ -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>({
- 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>;
-
-export const categorizedQueryResultsSelector = selector<
- Readonly
->({
- key: "categorizedQueryResults",
- get: ({ get }) => {
- const filteredResults = get(filteredQueryResultsSelector);
- return new Promise((resolve) =>
- resolve(
- filteredResults.reduce((acc, curr) => {
- curr.categories.forEach((category) => {
- if (!acc[category]) acc[category] = [];
- acc[category]!!.push(curr);
- });
- return acc;
- }, {})
- )
- );
- },
-});
-
-export const singleCategoryQueryResultsSelector = selectorFamily<
- ReadonlyArray,
- 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({
- key: "isDarkTheme",
- get: ({ get }) => TinyColor(get(iconColorAtom)).isLight(),
-});
-
-export const resetSettingsSelector = selector({
- key: "resetSettings",
- get: () => null,
- set: ({ reset }) => {
- reset(iconWeightAtom);
- reset(iconSizeAtom);
- reset(iconColorAtom);
- },
-});
diff --git a/src/utils/index.ts b/src/utils/index.ts
index 892f801..ef29ec1 100644
--- a/src/utils/index.ts
+++ b/src/utils/index.ts
@@ -30,29 +30,36 @@ export function getCodeSnippets({
const { r, g, b } = TinyColor(color).toRgb();
return {
- [SnippetType.HTML]: ``,
- [SnippetType.REACT]: `<${displayName} size={${size}} ${!isDefaultColor ? `color="${color}" ` : ""
- }${isDefaultWeight ? "" : `weight="${weight}" `}/>`,
- [SnippetType.VUE]: ``,
+ [SnippetType.HTML]: ``,
+ [SnippetType.REACT]: `<${displayName} size={${size}} ${
+ !isDefaultColor ? `color="${color}" ` : ""
+ }${isDefaultWeight ? "" : `weight="${weight}" `}/>`,
+ [SnippetType.VUE]: ``,
[SnippetType.FLUTTER]: `Icon(\n PhosphorIcons.${displayName.replace(
/^\w/,
(c) => c.toLowerCase()
- )}${isDefaultWeight ? "" : weight.replace(/^\w/, (c) => c.toUpperCase())
- },\n size: ${size.toFixed(1)},\n${!isDefaultColor ? ` color: Color(0xff${color.replace("#", "")}),\n` : ""
- })`,
- [SnippetType.ELM]: `Phosphor.${camelName}${isDefaultWeight ? "" : " " + pascalWeight
- }
+ )}${
+ isDefaultWeight ? "" : weight.replace(/^\w/, (c) => c.toUpperCase())
+ },\n size: ${size.toFixed(1)},\n${
+ !isDefaultColor ? ` color: Color(0xff${color.replace("#", "")}),\n` : ""
+ })`,
+ [SnippetType.ELM]: `Phosphor.${camelName}${
+ isDefaultWeight ? "" : " " + pascalWeight
+ }
|> withSize ${size}
|> withSizeUnit "px"
|> toHtml []`,
- [SnippetType.SWIFT]: `Ph.${camelName}.${weight}${!isDefaultColor
+ [SnippetType.SWIFT]: `Ph.${camelName}.${weight}${
+ !isDefaultColor
? `\n .color(red: ${u8ToCGFloatStr(r)}, green: ${u8ToCGFloatStr(
- g
- )}, blue: ${u8ToCGFloatStr(b)})`
+ g
+ )}, blue: ${u8ToCGFloatStr(b)})`
: ""
- }
+ }
.frame(width: ${size}, height: ${size})
`,
};
@@ -68,3 +75,40 @@ export function supportsWeight({
if (type !== SnippetType.FLUTTER) return true;
return weight !== IconStyle.DUOTONE;
}
+
+export function parseWeight(weight: string | null | undefined): IconStyle {
+ switch (weight?.replace('"', "").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 query?.replace('"', "") ?? "";
+}
+
+export function parseSize(size: string | null | undefined): number {
+ const sizeAsNumber = parseInt(size?.replace('"', "") ?? "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(color?.replace('"', "") ?? "#000000");
+ if (parsedColor.isValid()) {
+ return parsedColor.toHexString();
+ }
+ return "#000000";
+}