diff --git a/.gitignore b/.gitignore
index 7d3cec2..9866ef4 100644
--- a/.gitignore
+++ b/.gitignore
@@ -10,6 +10,7 @@
# production
/build
+/dist
# misc
.DS_Store
diff --git a/README.md b/README.md
index 95d71ae..5458d75 100644
--- a/README.md
+++ b/README.md
@@ -4,7 +4,7 @@
Phosphor is a flexible icon family for interfaces, diagrams, presentations — whatever, really.
-- 1047 icons and counting
+- 1,248 icons and counting
- 6 weights: **Thin**, **Light**, **Regular**, **Bold**, **Fill**, and **Duotone**
- Designed at 16 x 16px to read well small and scale up big
- Raw stroke information retained to fine-tune the style
@@ -13,9 +13,9 @@ More ways to use at [phosphoricons.com](https://phosphoricons.com).
## For developers
-Phosphor is available as a [one-liner](https://github.com/phosphor-icons/phosphor-icons) script, [React package](https://github.com/phosphor-icons/phosphor-react), and [Vue package](https://github.com/phosphor-icons/phosphor-vue), all of which can be sourced from NPM or from a CDN.
+Phosphor is available for [web](https://github.com/phosphor-icons/web), [React](https://github.com/phosphor-icons/react), [Vue](https://github.com/phosphor-icons/vue), [Flutter](https://github.com/phosphor-icons/flutter), [Elm](https://github.com/phosphor-icons/phosphor-elm), and other frameworks and platforms.
-### HTML/CSS
+### Vanilla Web
- **Simple to use** – We use a similar approach as many other icon sets out there, providing icons as a webfont that uses Unicode's Private Use Area character codes to map normally non-rendering characters to icons. But you don't need to know that. All you need to do is add the script to the document `
`, and drop in icons with an ` ` tag and the appropriate class:
@@ -23,26 +23,26 @@ Phosphor is available as a [one-liner](https://github.com/phosphor-icons/phospho
-
+
-
-
+
+
```
-Check out the full documentation on the [phosphor-icons](https://github.com/phosphor-icons/phosphor-icons) repo page.
+Check out the full documentation on the [@phosphor-icons/web](https://github.com/phosphor-icons/web) repo page.
### React
-- **Powerful** – Phosphor's intuitive but powerful API can style the `color`, `size`, and `weight` of an icon with a few keystrokes, provide default styles to all icons via the Context API, or directly manipulate the SVG at runtime through render props to do some amazing things! Check out the full documentation on the [phosphor-react](https://github.com/phosphor-icons/phosphor-react) repo page.
+- **Powerful** – Phosphor's intuitive but powerful API can style the `color`, `size`, and `weight` of an icon with a few keystrokes, provide default styles to all icons via the Context API, or directly manipulate the SVG at runtime through render props to do some amazing things! Check out the full documentation on the [@phosphor-icons/react](https://github.com/phosphor-icons/react) repo page.
```jsx
import React from "react";
import ReactDOM from "react-dom";
-import { Smiley, Heart, Horse } from "phosphor-react";
+import { Smiley, Heart, Horse } from "@phosphor-icons/react";
const App = () => {
return (
@@ -62,7 +62,7 @@ ReactDOM.render( , document.getElementById("root"));
### Vue
-- **Parity** – As with React, you can manipulate the `color`, `size`, and `weight` of an icon with a few keystrokes, or provide default styles to all icons via the `provide/inject` API. It is fully tree-shakable and ready to use right away. Check out the full documentation on the [phosphor-vue](https://github.com/phosphor-icons/phosphor-vue) repo page.
+- **Parity** – As with React, you can manipulate the `color`, `size`, and `weight` of an icon with a few keystrokes, or provide default styles to all icons via the `provide/inject` API. It is fully tree-shakable and ready to use right away. Check out the full documentation on the [@phosphor-icons/vue](https://github.com/phosphor-icons/vue) repo page.
```html
@@ -90,13 +90,15 @@ ReactDOM.render( , document.getElementById("root"));
## Our Related Projects
-- [phosphor-react](https://github.com/phosphor-icons/phosphor-react) ▲ Phosphor icon component library for React
-- [phosphor-vue](https://github.com/phosphor-icons/phosphor-vue) ▲ Phosphor icon component library for Vue
-- [phosphor-icons](https://github.com/phosphor-icons/phosphor-icons) ▲ Phosphor icons for Vanilla JS
-- [phosphor-flutter](https://github.com/phosphor-icons/phosphor-flutter) ▲ Phosphor IconData library for Flutter
-- [phosphor-webcomponents](https://github.com/phosphor-icons/phosphor-webcomponents) ▲ Phosphor icons as Web Components
-- [phosphor-figma](https://github.com/phosphor-icons/phosphor-figma) ▲ Phosphor icons Figma plugin
-- [phosphor-sketch](https://github.com/phosphor-icons/phosphor-sketch) ▲ Phosphor icons Sketch plugin
+- [@phosphor-icons/core](https://github.com/phosphor-icons/core) ▲ Phosphor icon assets and catalog
+- [@phosphor-icons/react](https://github.com/phosphor-icons/react) ▲ Phosphor icon component library for React
+- [@phosphor-icons/web](https://github.com/phosphor-icons/web) ▲ Phosphor icons for Vanilla JS
+- [@phosphor-icons/vue](https://github.com/phosphor-icons/vue) ▲ Phosphor icon component library for Vue
+- [@phosphor-icons/elm](https://github.com/phosphor-icons/phosphor-elm) ▲ Phosphor icons for Elm
+- [@phosphor-icons/flutter](https://github.com/phosphor-icons/flutter) ▲ Phosphor IconData library for Flutter
+- [@phosphor-icons/webcomponents](https://github.com/phosphor-icons/webcomponents) ▲ Phosphor icons as Web Components
+- [@phosphor-icons/figma](https://github.com/phosphor-icons/figma) ▲ Phosphor icons Figma plugin
+- [@phosphor-icons/sketch](https://github.com/phosphor-icons/sketch) ▲ Phosphor icons Sketch plugin
## Community Projects
diff --git a/bin/fetch.js b/bin/fetch.js
deleted file mode 100644
index bb31925..0000000
--- a/bin/fetch.js
+++ /dev/null
@@ -1,93 +0,0 @@
-#!/usr/bin/env node
-"use strict";
-
-const fs = require("fs/promises");
-const path = require("path");
-const axios = require("axios");
-const chalk = require("chalk");
-const { Command } = require("commander");
-const { version } = require("../package.json");
-
-const ICON_API_URL = "https://api.phosphoricons.com";
-
-async function main() {
- const program = new Command();
- program
- .version(version)
- .option(
- "-r --release ",
- "Fetch icons from Phosphor API and compile to internal data structure"
- )
- .option("-p --published", "Published status of icons")
- .option("-P, --no-published", "Published status of icons")
- .option("-q --query ", "Fulltext search term")
- .option("-n --name ", "Exact icon namee match");
-
- program.parse(process.argv);
- const params = new URLSearchParams(Object.entries(program.opts())).toString();
-
- try {
- const res = await axios.get(`${ICON_API_URL}?${params}`);
- if (res.data) {
- let fileString = `\
-import * as Icons from "phosphor-react";
-import { IconEntry, IconCategory } from ".";
-
-export const icons: ReadonlyArray = [
-`;
-
- res.data.icons.forEach((icon) => {
- let categories = "[";
- icon.searchCategories?.forEach((c) => {
- categories += `IconCategory.${c.toUpperCase()},`;
- });
- categories += "]";
-
- fileString += `\
- {
- name: "${icon.name}",
- categories: ${categories},
- tags: ${JSON.stringify(["*new*", ...icon.tags])},
- Icon: Icons.${icon.name
- .split("-")
- .map((substr) => substr.replace(/^\w/, (c) => c.toUpperCase()))
- .join("")},
- },
-`;
- console.log(`${chalk.inverse.green(" DONE ")} ${icon.name}`);
- });
-
- fileString += `
-];
-
-if (process.env.NODE_ENV === "development") {
- console.log(\`\${icons.length} icons\`);
-}
-
-export const iconCount = (icons.length * 6)
- .toString()
- .replace(/\B(?=(\d{3})+(?!\d))/g, ",");
-
-`;
-
- try {
- await fs.writeFile(
- path.join(__dirname, "../src/lib/new.ts"),
- fileString
- );
- console.log(
- `${chalk.green(" DONE ")} ${res.data.icons.length} icons ingested`
- );
- } catch (e) {
- console.error(`${chalk.inverse.red(" FAIL ")} Could not write file`);
- }
- } else {
- console.error(`${chalk.inverse.red(" FAIL ")} No data`);
- }
- } catch (e) {
- console.error(e);
- process.exit(-1);
- }
-}
-
-main();
diff --git a/public/index.html b/index.html
similarity index 92%
rename from public/index.html
rename to index.html
index 7fd8915..e67608f 100644
--- a/public/index.html
+++ b/index.html
@@ -3,7 +3,7 @@
Phosphor Icons
-
+
-
+
-
+
+
diff --git a/package.json b/package.json
index 74bc167..f4e8a74 100644
--- a/package.json
+++ b/package.json
@@ -1,6 +1,6 @@
{
"name": "phosphor-home",
- "version": "1.4.0",
+ "version": "2.0.0",
"license": "MIT",
"homepage": "https://phosphoricons.com",
"author": {
@@ -18,49 +18,41 @@
"UI",
"UX"
],
- "repository": "github:phosphor-icons/phosphor-home",
+ "repository": "github:phosphor-icons/homepage",
"private": true,
"scripts": {
- "analyze": "source-map-explorer 'build/static/js/*.js'",
- "start": "react-scripts start",
- "build": "react-scripts build",
- "test": "react-scripts test",
- "eject": "react-scripts eject",
- "fetch": "node ./bin/fetch.js",
+ "dev": "vite",
+ "build": "tsc && vite build",
+ "preview": "vite preview",
"format": "prettier --write \"./src/**/*.{js,jsx,ts,tsx,json,vue}\""
},
"dependencies": {
- "@phosphor-icons/core": "^1.4.5",
+ "@phosphor-icons/core": "^2.0.2",
+ "@phosphor-icons/react": "^2.0.4",
"file-saver": "^2.0.2",
- "framer-motion": "^3.10.0",
+ "framer-motion": "^9.0.1",
"fuse.js": "^6.4.1",
- "phosphor-react": "^1.4.0",
- "react": "^17.0.1",
- "react-dom": "^17.0.1",
+ "prop-types": "^15.8.1",
+ "react": "^18.2.0",
+ "react-dom": "^18.2.0",
"react-dropdown-select": "^4.4.2",
- "react-ga": "^3.1.2",
+ "react-ga4": "^2.0.0",
"react-hotkeys-hook": "^3.2.1",
- "react-scripts": "3.4.1",
- "react-use": "^15.3.2",
- "recoil": "^0.5.2",
- "svg2png-converter": "^1.0.0",
+ "react-use": "^17.4.0",
+ "recoil": "^0.7.6",
+ "svg2png-converter": "^1.0.2",
"tinycolor2": "^1.4.2"
},
"devDependencies": {
- "@testing-library/jest-dom": "^4.2.4",
- "@testing-library/react": "^9.3.2",
- "@testing-library/user-event": "^7.1.2",
- "@types/file-saver": "^2.0.1",
- "@types/jest": "^24.0.0",
- "@types/node": "^12.0.0",
- "@types/react": "^16.9.46",
- "@types/react-dom": "^16.9.8",
- "@types/react-virtualized": "^9.21.10",
- "@types/tinycolor2": "^1.4.2",
- "axios": "^0.24.0",
- "chalk": "^4",
- "commander": "^8.3.0",
- "typescript": "^3.9.6"
+ "@types/file-saver": "^2.0.5",
+ "@types/node": "^18.11.18",
+ "@types/react": "^18.0.27",
+ "@types/react-dom": "^18.0.10",
+ "@types/tinycolor2": "^1.4.3",
+ "@vitejs/plugin-react": "^3.1.0",
+ "typescript": "^4.9.5",
+ "vite": "^4.1.1",
+ "vite-plugin-svgr": "^2.4.0"
},
"eslintConfig": {
"extends": "react-app"
diff --git a/src/assets/u-arrow-up-left.svg b/src/assets/u-arrow-up-left.svg
deleted file mode 100644
index d77add4..0000000
--- a/src/assets/u-arrow-up-left.svg
+++ /dev/null
@@ -1,8 +0,0 @@
-
-
- u-arrow-up-left
-
-
-
-
-
\ No newline at end of file
diff --git a/src/components/App/App.css b/src/components/App/App.css
index 52f74e3..effc40e 100644
--- a/src/components/App/App.css
+++ b/src/components/App/App.css
@@ -1,3 +1,21 @@
+:root {
+ --red: #ff6e60;
+ --orange: #ff8e51;
+ --yellow: #ffd171;
+ --pale: #ffe8dc;
+ --peach: #ffd5c0;
+ --darkgreen: #245633;
+ --blue: #397fff;
+ --purple: #925bff;
+ --eggplant: #35313d;
+ --neutral: #86838b;
+ --translucent: rgba(163, 159, 171, 0.1);
+ --scrim: rgba(255, 255, 255, 0.05);
+ --sheer: rgba(194, 186, 196, 0.25);
+ --soft: rgba(194, 186, 196, 0.7);
+ --shadow: rgba(0, 0, 0, 0.15);
+}
+
body {
margin: 0px;
font-variant-ligatures: common-ligatures;
@@ -24,16 +42,14 @@ img {
pre,
code {
font-family: "IBM Plex Mono", "Courier New", monospace;
- font-size: 14px;
+ font-size: 12px;
}
pre {
box-sizing: border-box;
- padding: 20px 16px 20px 24px;
- margin: 12px 0px;
- background-color: white;
+ margin: 0;
border-radius: 6px;
- border: 1px solid #e1d4d7;
+ font-size: 12x;
white-space: pre-wrap;
}
@@ -47,6 +63,7 @@ button {
display: flex;
align-items: center;
justify-content: flex-start;
+ cursor: pointer;
}
button.main-button {
@@ -77,14 +94,6 @@ button.main-button:active {
box-shadow: 0 0 0 0 black;
}
-button.main-button:focus {
- outline: none;
-}
-
-/* button.main-button:not(:last-child) {
- margin: 0 24px 24px 0;
-} */
-
button.main-button svg {
margin-right: 12px;
}
@@ -117,14 +126,29 @@ a.main-link:hover:after {
}
.badge.new {
- color: #ff6e60;
+ color: var(--red);
}
.badge.updated {
- color: #397fff;
+ color: var(--blue);
}
.badge {
font-size: 24px;
line-height: 0.5em;
}
+
+.card {
+ border-radius: 8px;
+ border: 2px solid var(--translucent);
+}
+
+.primary {
+ color: var(--foreground);
+ background-color: var(--background);
+}
+
+.secondary {
+ color: var(--foreground-card);
+ background-color: var(--background-card);
+}
diff --git a/src/components/App/App.test.tsx b/src/components/App/App.test.tsx
deleted file mode 100644
index 352d7b8..0000000
--- a/src/components/App/App.test.tsx
+++ /dev/null
@@ -1,9 +0,0 @@
-import React from "react";
-import { render } from "@testing-library/react";
-import App from "./App";
-
-test("renders learn react link", () => {
- const { getByText } = render( );
- const linkElement = getByText(/learn react/i);
- expect(linkElement).toBeInTheDocument();
-});
diff --git a/src/components/App/App.tsx b/src/components/App/App.tsx
index a5e3c0d..6d30762 100644
--- a/src/components/App/App.tsx
+++ b/src/components/App/App.tsx
@@ -1,14 +1,19 @@
-import React, { Suspense } from "react";
+import { Fragment, Suspense, useMemo } from "react";
+import { useRecoilValue } from "recoil";
import "./App.css";
-import Header from "../Header/Header";
-import Toolbar from "../Toolbar/Toolbar";
-import IconGrid from "../IconGrid/IconGrid";
-import Footer from "../Footer/Footer";
-import ErrorBoundary from "../ErrorBoundary/ErrorBoundary";
-import Notice from "../Notice/Notice";
-import useIconParameters from "../../hooks/useIconParameters";
-import usePersistSettings from "../../hooks/usePersistSettings";
+import Header from "@/components/Header";
+import Toolbar from "@/components/Toolbar";
+import IconGrid from "@/components/IconGrid";
+import Footer from "@/components/Footer";
+import ErrorBoundary from "@/components/ErrorBoundary";
+import Notice from "@/components/Notice";
+import {
+ useIconParameters,
+ usePersistSettings,
+ useCSSVariables,
+} from "@/hooks";
+import { isDarkThemeSelector } from "@/state";
const errorFallback = ;
const waitingFallback = ;
@@ -16,9 +21,23 @@ const waitingFallback = ;
const App: React.FC = () => {
useIconParameters();
usePersistSettings();
+
+ const isDark = useRecoilValue(isDarkThemeSelector);
+
+ const properties = useMemo(
+ () => ({
+ "--foreground": isDark ? "white" : "black",
+ "--foreground-card": isDark ? "white" : "#35313D",
+ "--background": isDark ? "#35313D" : "white",
+ "--background-card": isDark ? "#413c48" : "#f6f5f6",
+ }),
+ [isDark]
+ );
+
+ useCSSVariables(properties);
return (
-
+
@@ -29,7 +48,7 @@ const App: React.FC = () => {
-
+
);
};
diff --git a/src/components/App/index.ts b/src/components/App/index.ts
new file mode 100644
index 0000000..8ce017e
--- /dev/null
+++ b/src/components/App/index.ts
@@ -0,0 +1 @@
+export { default } from "./App";
diff --git a/src/components/Banner/Banner.css b/src/components/Banner/Banner.css
index 4e91d19..594b9da 100644
--- a/src/components/Banner/Banner.css
+++ b/src/components/Banner/Banner.css
@@ -1,10 +1,15 @@
.banner {
- display: grid;
- place-items: center;
- padding: 24px;
+ position: fixed;
+ top: 0;
+ left: 0;
+ right: 0;
+ border-radius: 0;
+ display: flex;
+ padding: 12px;
color: white;
- text-align: center;
- background-color: #35313d;
+ margin: auto;
+ background-color: var(--eggplant);
+ z-index: 1;
}
.banner .main-button {
@@ -12,3 +17,39 @@
min-height: 64px;
margin: 8px 0 0;
}
+
+.banner a {
+ color: white;
+}
+
+.banner-content {
+ display: flex;
+ align-items: center;
+ justify-content: space-between;
+ gap: 20px;
+ flex: 1;
+ max-width: 1120px;
+ margin: auto;
+ font-family: "IBM Plex Mono";
+}
+
+.banner-button {
+ color: inherit;
+ background: var(--eggplant);
+ height: unset !important;
+ padding: 0 !important;
+ margin: 0 !important;
+ border-radius: 48px !important;
+ cursor: pointer;
+}
+
+.banner-button:active {
+ opacity: 0.7;
+}
+
+.message {
+ display: grid;
+ grid-template-columns: 32px 1fr;
+ align-items: center;
+ gap: 20px;
+}
diff --git a/src/components/Banner/Banner.tsx b/src/components/Banner/Banner.tsx
index af87e19..8fcc873 100644
--- a/src/components/Banner/Banner.tsx
+++ b/src/components/Banner/Banner.tsx
@@ -1,27 +1,80 @@
-import React from "react";
-import { Medal } from "phosphor-react";
-import ReactGA from "react-ga";
+import { ReactNode, Dispatch, SetStateAction } from "react";
+import { motion, AnimatePresence, Variants } from "framer-motion";
+import { XCircle } from "@phosphor-icons/react";
+import ReactGA from "react-ga4";
+
+import { useLocalStorage } from "@/hooks";
import "./Banner.css";
-const Banner = () => {
- const handleClick = () => {
- ReactGA.event({ category: "Outbound", action: "Click", label: "Vote" });
- window.open(
- "https://www.figma.com/community/file/903830135544202908",
- "_blank",
- "noopener noreferrer"
- );
+type BannerState = {
+ seen: Record;
+};
+
+type BannerProps = {
+ id: string;
+ children?: ReactNode;
+ onClose?: (dispatch: Dispatch>) => void;
+};
+
+const variants: Variants = {
+ initial: { y: -120 },
+ animate: { y: 0 },
+ exit: { y: -120 },
+};
+
+const BANNER_STATE_KEY = "banner_state";
+
+const Banner = ({ id, children, onClose }: BannerProps) => {
+ const [
+ {
+ seen: { [id]: seen },
+ },
+ setBannerState,
+ ] = useLocalStorage(BANNER_STATE_KEY, {
+ seen: { [id]: false },
+ });
+
+ const handleClose = () => {
+ ReactGA.event({
+ category: "Banner",
+ action: "Dismiss",
+ label: id,
+ });
+ onClose
+ ? onClose(setBannerState)
+ : setBannerState((state) => ({
+ ...state,
+ seen: { ...state.seen, [id]: true },
+ }));
};
return (
-
- The 2022 Figma Community Awards are here!
-
-
- Vote for Phosphor
-
-
+
+ {!seen && (
+
+
+ {children}
+ {
+ e.key === "Enter" && handleClose();
+ }}
+ >
+
+
+
+
+ )}
+
);
};
diff --git a/src/components/Banner/index.ts b/src/components/Banner/index.ts
new file mode 100644
index 0000000..907b6e0
--- /dev/null
+++ b/src/components/Banner/index.ts
@@ -0,0 +1 @@
+export { default } from "./Banner";
diff --git a/src/components/ColorInput/ColorInput.tsx b/src/components/ColorInput/ColorInput.tsx
index b98df2a..4f9201c 100644
--- a/src/components/ColorInput/ColorInput.tsx
+++ b/src/components/ColorInput/ColorInput.tsx
@@ -1,14 +1,14 @@
-import React, { useCallback } from "react";
+import { useCallback } from "react";
import { useRecoilState, useRecoilValue } from "recoil";
-import { iconColorAtom } from "../../state/atoms";
-import { isDarkThemeSelector } from "../../state/selectors";
-import useThrottled from "../../hooks/useThrottled";
+import { useThrottled } from "@/hooks";
+import { iconColorAtom, isDarkThemeSelector } from "@/state";
+
import "./ColorInput.css";
type ColorInputProps = {};
-const ColorInput: React.FC = () => {
+const ColorInput = (_: ColorInputProps) => {
const [color, setColor] = useRecoilState(iconColorAtom);
const isDark = useRecoilValue(isDarkThemeSelector);
diff --git a/src/components/ColorInput/index.ts b/src/components/ColorInput/index.ts
new file mode 100644
index 0000000..ea57087
--- /dev/null
+++ b/src/components/ColorInput/index.ts
@@ -0,0 +1 @@
+export { default } from "./ColorInput";
diff --git a/src/components/ErrorBoundary/ErrorBoundary.tsx b/src/components/ErrorBoundary/ErrorBoundary.tsx
index 40bdf9b..4922981 100644
--- a/src/components/ErrorBoundary/ErrorBoundary.tsx
+++ b/src/components/ErrorBoundary/ErrorBoundary.tsx
@@ -1,14 +1,15 @@
-import React, { ErrorInfo } from "react";
+import { Component, ErrorInfo, ReactNode } from "react";
interface ErrorBoundaryProps {
- fallback?: JSX.Element | React.ReactNode;
+ fallback?: JSX.Element | ReactNode;
+ children?: JSX.Element | ReactNode;
}
interface ErrorBoundaryState {
errorMessage?: string;
}
-export default class ErrorBoundary extends React.Component<
+export default class ErrorBoundary extends Component<
ErrorBoundaryProps,
ErrorBoundaryState
> {
@@ -26,7 +27,7 @@ export default class ErrorBoundary extends React.Component<
console.info(info);
}
- render(): JSX.Element | React.ReactNode {
+ render(): JSX.Element | ReactNode {
if (this.state.errorMessage) {
return this.props.fallback ?? {this.state.errorMessage}
;
}
diff --git a/src/components/ErrorBoundary/index.ts b/src/components/ErrorBoundary/index.ts
new file mode 100644
index 0000000..3406bab
--- /dev/null
+++ b/src/components/ErrorBoundary/index.ts
@@ -0,0 +1 @@
+export { default } from "./ErrorBoundary";
diff --git a/src/components/Footer/Footer.css b/src/components/Footer/Footer.css
index 5168379..60a3601 100644
--- a/src/components/Footer/Footer.css
+++ b/src/components/Footer/Footer.css
@@ -1,5 +1,5 @@
footer {
- background-color: #925bff;
+ background-color: var(--purple);
}
#back-to-top-button {
@@ -8,6 +8,16 @@ footer {
margin: 0;
border-radius: 50%;
z-index: 2;
+ font-size: 56px;
+}
+
+#back-to-top-button:active {
+ transform: translate(4px, 4px) !important;
+ box-shadow: 0 0 0 0 black;
+}
+
+#back-to-top-button svg {
+ margin-right: unset;
}
.container {
@@ -65,11 +75,7 @@ footer .links {
width: 56px;
height: 56px;
padding: 0;
- }
-
- #back-to-top-button img {
- width: 28px;
- height: 28px;
+ font-size: 28px;
}
footer .links {
@@ -133,17 +139,11 @@ footer .links {
top: 276px;
}
- /* #command {
- position: absolute;
- left: 532px;
- top: 150px;
- } */
-
.illustrations-footer {
display: initial;
position: absolute;
left: -240px;
- top: 656px;
+ top: 632px;
height: 584px;
overflow: hidden;
}
diff --git a/src/components/Footer/Footer.tsx b/src/components/Footer/Footer.tsx
index 3112c44..58b1b0c 100644
--- a/src/components/Footer/Footer.tsx
+++ b/src/components/Footer/Footer.tsx
@@ -1,30 +1,51 @@
-import React from "react";
-import { Coffee, Heart } from "phosphor-react";
+import { useRecoilValue } from "recoil";
+import { motion, AnimatePresence, Variants } from "framer-motion";
+import { Coffee, Heart, ArrowULeftUp } from "@phosphor-icons/react";
-import uArrowUpLeft from "../../assets/u-arrow-up-left.svg";
-import markerGreen from "../../assets/marker-green.svg";
-import postIt from "../../assets/footer-mobile.svg";
-import Links from "../Links/Links";
+import Links from "@/components/Links/Links";
+
+import { ReactComponent as MarkerGreen } from "@/assets/marker-green.svg";
+import { ReactComponent as PostIt } from "@/assets/footer-mobile.svg";
+import { useMediaQuery } from "@/hooks";
+import { selectionEntryAtom } from "@/state";
import "./Footer.css";
type FooterProps = {};
-const Footer: React.FC = () => {
+const variants: Variants = {
+ initial: { y: 188 },
+ animate: { y: 0 },
+ exit: { y: 188 },
+};
+
+const Footer = (_: FooterProps) => {
+ const isMobile = useMediaQuery("(max-width: 719px)");
+ const isViewing = !!useRecoilValue(selectionEntryAtom);
+
return (
-
{
- document
- .getElementById("root")
- ?.scrollIntoView({ behavior: "smooth", block: "start" });
- }}
- >
-
-
+
+ {(!isMobile || !isViewing) && (
+ {
+ document
+ .getElementById("root")
+ ?.scrollIntoView({ behavior: "smooth", block: "start" });
+ }}
+ >
+
+
+ )}
+
-
+
diff --git a/src/components/Footer/index.ts b/src/components/Footer/index.ts
new file mode 100644
index 0000000..3738288
--- /dev/null
+++ b/src/components/Footer/index.ts
@@ -0,0 +1 @@
+export { default } from "./Footer";
diff --git a/src/components/Header/Header.css b/src/components/Header/Header.css
index 844ba6c..f54fdad 100644
--- a/src/components/Header/Header.css
+++ b/src/components/Header/Header.css
@@ -1,7 +1,7 @@
header {
- width: 100%;
- background-color: #ffd171;
overflow: hidden;
+ position: relative;
+ background-color: var(--yellow);
}
.header-contents {
diff --git a/src/components/Header/Header.tsx b/src/components/Header/Header.tsx
index fd2cce6..7bb4da9 100644
--- a/src/components/Header/Header.tsx
+++ b/src/components/Header/Header.tsx
@@ -1,29 +1,35 @@
-import React from "react";
-import { ArrowCircleUpRight, ArrowCircleDown } from "phosphor-react";
+import {
+ ArrowCircleUpRight,
+ ArrowCircleDown,
+ Broadcast,
+} from "@phosphor-icons/react";
-import markerPurple from "../../assets/marker-purple.svg";
-import paperclips from "../../assets/paperclips-header-mobile.svg";
-import paperclipsThree from "../../assets/paperclips-header.svg";
-import tablet from "../../assets/tablet.svg";
-import tabletSpec from "../../assets/tablet-spec.svg";
-import billiardBall from "../../assets/billiard-ball.svg";
-import billiardBallSpec from "../../assets/billiard-ball-spec.svg";
-import warning from "../../assets/warning.svg";
-import warningSpec from "../../assets/warning-spec.svg";
-import cuttingMat from "../../assets/cutting-mat.svg";
-import cuttingMatSpec from "../../assets/cutting-mat-spec.svg";
-import receipt from "../../assets/receipt.svg";
-import receiptSpec from "../../assets/receipt-spec.svg";
-import calculator from "../../assets/calculator.svg";
-import calculatorSpec from "../../assets/calculator-spec.svg";
-import Links from "../Links/Links";
+import Banner from "@/components/Banner";
+
+import { ReactComponent as MarkerPurple } from "@/assets/marker-purple.svg";
+import { ReactComponent as PaperClips } from "@/assets/paperclips-header-mobile.svg";
+import { ReactComponent as PaperClipsThree } from "@/assets/paperclips-header.svg";
+import { ReactComponent as Tablet } from "@/assets/tablet.svg";
+import { ReactComponent as TabletSpec } from "@/assets/tablet-spec.svg";
+import { ReactComponent as BilliardBall } from "@/assets/billiard-ball.svg";
+import { ReactComponent as BilliardBallSpec } from "@/assets/billiard-ball-spec.svg";
+import { ReactComponent as Warning } from "@/assets/warning.svg";
+import { ReactComponent as WarningSpec } from "@/assets/warning-spec.svg";
+import { ReactComponent as CuttingMat } from "@/assets/cutting-mat.svg";
+import { ReactComponent as CuttingMatSpec } from "@/assets/cutting-mat-spec.svg";
+import { ReactComponent as Receipt } from "@/assets/receipt.svg";
+import { ReactComponent as ReceiptSpec } from "@/assets/receipt-spec.svg";
+import { ReactComponent as Calculator } from "@/assets/calculator.svg";
+import { ReactComponent as CalculatorSpec } from "@/assets/calculator-spec.svg";
+
+import Links from "@/components/Links";
import "./Header.css";
type HeaderProps = {};
const handleGetStarted = () =>
window.open(
- "https://github.com/phosphor-icons/phosphor-home#phosphor-icons",
+ "https://github.com/phosphor-icons/homepage#phosphor-icons",
"_blank",
"noopener noreferrer"
);
@@ -33,25 +39,34 @@ const handleScrollToIcons = () =>
.getElementById("toolbar")
?.scrollIntoView({ behavior: "smooth", block: "start" });
-const Header: React.FC = () => {
+const Header = (_: HeaderProps) => {
return (
diff --git a/src/components/Header/index.ts b/src/components/Header/index.ts
new file mode 100644
index 0000000..2764567
--- /dev/null
+++ b/src/components/Header/index.ts
@@ -0,0 +1 @@
+export { default } from "./Header";
diff --git a/src/components/IconGrid/DetailFooter.tsx b/src/components/IconGrid/DetailFooter.tsx
new file mode 100644
index 0000000..b21c321
--- /dev/null
+++ b/src/components/IconGrid/DetailFooter.tsx
@@ -0,0 +1,304 @@
+import React, { useRef, useEffect, CSSProperties, useMemo } from "react";
+import { useRecoilValue, useRecoilState } from "recoil";
+import { useHotkeys } from "react-hotkeys-hook";
+import { motion, AnimatePresence, Variants } from "framer-motion";
+import { Svg2Png } from "svg2png-converter";
+import { saveAs } from "file-saver";
+import {
+ Copy,
+ CheckCircle,
+ DownloadSimple,
+ XCircle,
+} from "@phosphor-icons/react";
+import ReactGA from "react-ga4";
+
+import Tabs, { Tab } from "@/components/Tabs";
+import { useMediaQuery, useTransientState, useSessionStorage } from "@/hooks";
+import { SnippetType } from "@/lib";
+import {
+ iconWeightAtom,
+ iconSizeAtom,
+ iconColorAtom,
+ selectionEntryAtom,
+ isDarkThemeSelector,
+} from "@/state";
+import { getCodeSnippets, supportsWeight } from "@/utils";
+
+import TagCloud from "./TagCloud";
+
+const variants: Record = {
+ desktop: {
+ initial: { y: 188 },
+ animate: { y: 0 },
+ exit: { y: 188 },
+ },
+ mobile: {
+ initial: { y: "60vh" },
+ animate: { y: 0 },
+ exit: { y: "60vh" },
+ },
+};
+
+const RENDERED_SNIPPETS = [
+ SnippetType.REACT,
+ SnippetType.VUE,
+ SnippetType.HTML,
+ SnippetType.FLUTTER,
+ SnippetType.ELM,
+];
+
+const buttonColor = "#35313D";
+const successColor = "#1FA647";
+const disabledColor = "#B7B7B7";
+
+function cloneWithSize(svg: SVGSVGElement, size: number): SVGSVGElement {
+ const sized = svg.cloneNode(true) as SVGSVGElement;
+ sized.setAttribute("width", `${size}`);
+ sized.setAttribute("height", `${size}`);
+ return sized;
+}
+
+const DetailFooter = () => {
+ const [entry, setSelectionEntry] = useRecoilState(selectionEntryAtom);
+
+ const weight = useRecoilValue(iconWeightAtom);
+ const size = useRecoilValue(iconSizeAtom);
+ const color = useRecoilValue(iconColorAtom);
+ const isDark = useRecoilValue(isDarkThemeSelector);
+ const [copied, setCopied] = useTransientState(
+ false,
+ 2000
+ );
+ const ref = useRef(null);
+
+ const [{ i }, setInitialTab] = useSessionStorage("tab", { i: 0 });
+
+ const isMobile = useMediaQuery("(max-width: 719px)");
+
+ const [snippets, tabs] = useMemo<
+ [Partial>, Tab[]]
+ >(() => {
+ if (!entry) return [{}, []];
+
+ const snippets = getCodeSnippets({
+ displayName: entry?.pascal_name!,
+ name: entry.name,
+ weight,
+ size,
+ color,
+ });
+
+ const snippetButtonStyle: CSSProperties =
+ weight === "duotone"
+ ? { color: disabledColor, userSelect: "none" }
+ : { color: "currentcolor" };
+
+ const tabs = [
+ {
+ header: "Tags",
+ content: (
+ ([
+ ...entry.categories,
+ ...entry.name.split("-"),
+ ...entry.tags,
+ ])
+ )}
+ />
+ ),
+ },
+ ].concat(
+ RENDERED_SNIPPETS.map((type) => {
+ const isWeightSupported = supportsWeight({ type, weight });
+
+ return {
+ header: type,
+ content: (
+
+
+
+ {isWeightSupported
+ ? snippets[type]
+ : "This weight is not yet supported"}
+
+ handleCopySnippet(e, type)}
+ disabled={!isWeightSupported}
+ style={
+ isWeightSupported
+ ? { color: "currentColor" }
+ : snippetButtonStyle
+ }
+ >
+ {copied === type ? (
+
+ ) : (
+
+ )}
+
+
+
+ ),
+ };
+ })
+ );
+
+ return [snippets, tabs];
+ }, [entry, weight, size, copied, isDark]);
+
+ useHotkeys("esc", () => setSelectionEntry(null));
+
+ useEffect(() => {
+ if (!entry) return;
+ ReactGA.event({
+ category: "Grid",
+ action: "Details",
+ label: entry.name,
+ });
+ }, [entry]);
+
+ const buttonBarStyle: CSSProperties = {
+ color: isDark ? "white" : buttonColor,
+ backgroundColor: "transparent",
+ };
+
+ const handleCopySnippet = (
+ event: React.MouseEvent,
+ type: SnippetType
+ ) => {
+ event.currentTarget.blur();
+ if (!entry) return;
+
+ setCopied(type);
+ const data = snippets[type];
+ data && void navigator.clipboard?.writeText(data);
+ };
+
+ const handleCopySVG = (
+ event: React.MouseEvent
+ ) => {
+ event.currentTarget.blur();
+ if (!entry) return;
+ if (!ref.current) return;
+
+ navigator.clipboard?.writeText(cloneWithSize(ref.current, size).outerHTML);
+ setCopied("SVG");
+ };
+
+ const handleDownloadSVG = (
+ event: React.MouseEvent
+ ) => {
+ event.currentTarget.blur();
+ if (!entry) return;
+ if (!ref.current) return;
+
+ const blob = new Blob([cloneWithSize(ref.current, size).outerHTML]);
+ saveAs(
+ blob,
+ `${entry?.name}${weight === "regular" ? "" : `-${weight}`}.svg`
+ );
+ };
+
+ const handleDownloadPNG = async (
+ event: React.MouseEvent
+ ) => {
+ event.currentTarget.blur();
+ if (!entry) return;
+ if (!ref.current) return;
+
+ Svg2Png.save(
+ cloneWithSize(ref.current, size),
+ `${entry?.name}${weight === "regular" ? "" : `-${weight}`}.png`
+ );
+ };
+
+ return (
+
+ {!!entry && (
+
+
+
+
+
+ {entry.name}
+
+ available in v{entry.published_in.toFixed(1)}.0+
+
+
+
+
+
+ {" "}
+ PNG
+
+
+ {" "}
+ SVG
+
+
+ {copied === "SVG" ? (
+
+ ) : (
+
+ )}
+ {copied === "SVG" ? "Copied!" : " SVG"}
+
+
+
+
+ setInitialTab({ i })}
+ />
+
+ setSelectionEntry(null)}
+ onKeyDown={(e) => {
+ e.key === "Enter" && setSelectionEntry(null);
+ }}
+ >
+
+
+
+ )}
+
+ );
+};
+
+export default DetailFooter;
diff --git a/src/components/IconGrid/DetailsPanel.tsx b/src/components/IconGrid/DetailsPanel.tsx
deleted file mode 100644
index 8d2c88a..0000000
--- a/src/components/IconGrid/DetailsPanel.tsx
+++ /dev/null
@@ -1,254 +0,0 @@
-import React, { useRef, useEffect } from "react";
-import { useRecoilValue, useSetRecoilState } from "recoil";
-import { useHotkeys } from "react-hotkeys-hook";
-import { motion } from "framer-motion";
-import { Svg2Png } from "svg2png-converter";
-import { saveAs } from "file-saver";
-import { Copy, X, CheckCircle, Download } from "phosphor-react";
-import ReactGA from "react-ga";
-
-import {
- iconWeightAtom,
- iconSizeAtom,
- iconColorAtom,
- iconPreviewOpenAtom,
-} from "../../state/atoms";
-import useTransientState from "../../hooks/useTransientState";
-import TagCloud from "./TagCloud";
-import { IconEntry, SnippetType } from "../../lib";
-import { getCodeSnippets, supportsWeight } from "../../utils";
-
-const panelVariants = {
- open: {
- opacity: 1,
- height: "100%",
- marginTop: "4px",
- marginBottom: "4px",
- transition: { type: "tween", duration: 0.1 },
- },
- collapsed: {
- opacity: 0,
- height: "0px",
- marginTop: "0px",
- marginBottom: "0px",
- transition: { type: "tween", duration: 0.1 },
- },
-};
-
-const contentVariants = {
- open: { opacity: 1, transition: { duration: 0.2, delay: 0.1 } },
- collapsed: { opacity: 0, transition: { duration: 0 } },
-};
-
-const buttonColor = "#35313D";
-const successColor = "#1FA647";
-const disabledColor = "#B7B7B7";
-
-interface InfoPanelProps {
- index: number;
- spans: number;
- isDark: boolean;
- entry: IconEntry;
-}
-
-const renderedSnippets = [
- SnippetType.REACT,
- SnippetType.VUE,
- SnippetType.HTML,
- SnippetType.FLUTTER,
-];
-
-const DetailsPanel: React.FC = (props) => {
- const { index, spans, isDark, entry } = props;
- const { name, Icon, categories, tags } = entry;
- const weight = useRecoilValue(iconWeightAtom);
- const size = useRecoilValue(iconSizeAtom);
- const color = useRecoilValue(iconColorAtom);
- const setOpen = useSetRecoilState(iconPreviewOpenAtom);
- const [copied, setCopied] = useTransientState(
- false,
- 2000
- );
- const ref = useRef(null);
-
- useHotkeys("esc", () => setOpen(false));
-
- useEffect(
- () => ReactGA.event({ category: "Grid", action: "Details", label: name }),
- [name]
- );
-
- const buttonBarStyle: React.CSSProperties = {
- color: isDark ? "white" : buttonColor,
- };
- const snippetButtonStyle: React.CSSProperties =
- weight === "duotone"
- ? { color: disabledColor, userSelect: "none" }
- : { color: buttonColor };
-
- const snippets = getCodeSnippets({
- displayName: Icon.displayName!,
- name,
- weight,
- size,
- color,
- });
-
- const handleCopySnippet = (
- event: React.MouseEvent,
- type: SnippetType
- ) => {
- event.currentTarget.blur();
- setCopied(type);
- const data = snippets[type];
- data && void navigator.clipboard?.writeText(data);
- };
-
- const handleCopySVG = (
- event: React.MouseEvent
- ) => {
- event.currentTarget.blur();
- setCopied("SVG");
- ref.current && void navigator.clipboard?.writeText(ref.current.outerHTML);
- };
-
- const handleDownloadSVG = (
- event: React.MouseEvent
- ) => {
- event.currentTarget.blur();
- if (!ref.current?.outerHTML) return;
- const blob = new Blob([ref.current.outerHTML]);
- saveAs(blob, `${name}${weight === "regular" ? "" : `-${weight}`}.svg`);
- };
-
- const handleDownloadPNG = async (
- event: React.MouseEvent
- ) => {
- event.currentTarget.blur();
- if (!ref.current?.outerHTML) return;
- Svg2Png.save(
- ref.current,
- `${name}${weight === "regular" ? "" : `-${weight}`}.png`,
- { scaleX: 2.667, scaleY: 2.667 }
- );
- };
-
- return (
-
-
-
- {name}
- ([...categories, ...name.split("-"), ...tags])
- )}
- isDark={isDark}
- />
-
-
-
- {renderedSnippets.map((type) => {
- const isWeightSupported = supportsWeight({ type, weight });
-
- return (
-
- {type}
-
-
- {isWeightSupported
- ? snippets[type]
- : "This weight is not yet supported"}
-
- handleCopySnippet(e, type)}
- disabled={!isWeightSupported}
- style={isWeightSupported ? undefined : snippetButtonStyle}
- >
- {copied === type ? (
-
- ) : (
-
- )}
-
-
-
- );
- })}
-
-
-
- Download
- PNG
-
-
- Download
- SVG
-
-
- {copied === "SVG" ? (
-
- ) : (
-
- )}
- {copied === "SVG" ? "Copied!" : "Copy SVG"}
-
-
-
-
- setOpen(false)}
- onKeyDown={(e) => {
- e.key === "Enter" && setOpen(false);
- }}
- />
-
-
- );
-};
-
-export default DetailsPanel;
diff --git a/src/components/IconGrid/IconGrid.css b/src/components/IconGrid/IconGrid.css
index fb61f4a..2345f8e 100644
--- a/src/components/IconGrid/IconGrid.css
+++ b/src/components/IconGrid/IconGrid.css
@@ -1,7 +1,11 @@
.grid-container {
+ position: relative;
padding: 32px 16px;
- min-height: 80vh;
+ /* min-height: 80vh; */
+ z-index: 1;
content-visibility: auto;
+ color: var(--foreground);
+ background-color: var(--background);
}
.grid {
@@ -26,22 +30,21 @@
-webkit-user-select: none;
user-select: none;
cursor: pointer;
- /* transition: background-color 100ms ease; */
}
.grid-item:hover {
- background-color: rgba(163, 159, 171, 0.1);
+ background-color: var(--translucent);
}
.grid-item:focus {
outline: none;
- border: 2px solid rgba(163, 159, 171, 0.1);
+ border: 2px solid var(--translucent);
}
.grid-item p {
font-size: 12px;
line-height: 16px;
- color: #86838b;
+ color: var(--neutral);
margin-top: 12px;
text-align: center;
}
@@ -64,59 +67,19 @@
}
}
-.info-box {
- position: relative;
- display: flex;
- width: 100%;
- height: 0px;
- margin: 0 4px;
- border-radius: 16px;
- background-color: rgba(163, 159, 171, 0.1);
-}
-
-@media screen and (max-width: 1023px) {
- .icon-preview {
- display: none !important;
- }
-
- .icon-usage {
- padding-left: 10% !important;
- }
-
- .snippet pre {
- padding: 12px 8px 12px 20px;
- }
-}
-
-.icon-preview {
- width: 30%;
- display: flex;
- text-align: center;
- flex-direction: column;
- align-items: center;
- margin-top: 72px;
-}
-
-.icon-preview p {
- margin: 0;
- font-size: 12px;
- line-height: 16px;
-}
-
-.icon-usage {
- flex: 1;
- padding: 56px 10% 56px 0;
+.versioning {
+ margin-top: 2px;
+ opacity: 0.6;
}
.snippet {
- margin-bottom: 24px;
+ width: 100%;
}
.snippet pre {
display: flex;
- align-items: center;
+ align-items: flex-start;
text-overflow: ellipsis;
- color: black;
-moz-user-select: all;
-webkit-user-select: all;
user-select: all;
@@ -171,12 +134,42 @@
.close-icon {
position: absolute;
- top: 24px;
- right: 24px;
+ top: 12px;
+ right: 12px;
text-align: end;
cursor: pointer;
}
+.close-button {
+ color: inherit;
+ background: transparent;
+ height: unset !important;
+ padding: 0 !important;
+ margin: 0 !important;
+ border-radius: 48px !important;
+ position: absolute;
+ top: -14px;
+ right: -18px;
+ text-align: end;
+ cursor: pointer;
+}
+
+.close-button::before {
+ content: "";
+ background: var(--background);
+ position: absolute;
+ width: 18px;
+ height: 18px;
+ top: 5px;
+ left: 5px;
+ border-radius: 50%;
+ z-index: -1;
+}
+
+.close-button:active {
+ opacity: 0.7;
+}
+
.empty-list {
display: flex;
flex-direction: column;
@@ -201,3 +194,66 @@
position: relative;
top: -96px;
}
+
+aside.detail-footer {
+ position: sticky;
+ bottom: 16px;
+ margin: auto;
+ max-width: 1120px;
+ display: grid;
+ grid-template-columns: 232px 1fr;
+ gap: 24px;
+ padding: 12px 24px;
+ height: 136px;
+}
+
+figure {
+ margin: 0;
+ display: grid;
+ grid-template-columns: 64px 1fr;
+ gap: 24px;
+ align-items: center;
+}
+
+figcaption {
+ display: flex;
+ flex-direction: column;
+ font-size: 14px;
+}
+
+figcaption > p {
+ margin: 0;
+}
+
+.detail-preview {
+ display: flex;
+ flex-direction: column;
+ justify-content: center;
+ gap: 24px;
+}
+
+.detail-actions {
+ display: flex;
+ flex-direction: row;
+ align-items: center;
+ gap: 8px;
+}
+
+@media screen and (max-width: 719px) {
+ .close-button {
+ top: 4px;
+ right: 12px;
+ }
+
+ aside.detail-footer {
+ top: 16px;
+ bottom: -4px;
+ display: flex;
+ flex-direction: column;
+ height: 440px;
+ }
+}
+
+.action-button svg {
+ margin-right: 6px;
+}
diff --git a/src/components/IconGrid/IconGrid.tsx b/src/components/IconGrid/IconGrid.tsx
index 806dcad..5bc3e0d 100644
--- a/src/components/IconGrid/IconGrid.tsx
+++ b/src/components/IconGrid/IconGrid.tsx
@@ -1,17 +1,20 @@
-import React, { useRef, useEffect } from "react";
+import { useRef, useEffect } from "react";
import { useRecoilValue } from "recoil";
import { motion, useAnimation } from "framer-motion";
-import { IconContext } from "phosphor-react";
+import { IconContext } from "@phosphor-icons/react";
-import { iconWeightAtom, iconSizeAtom, iconColorAtom } from "../../state/atoms";
import {
+ iconWeightAtom,
+ iconSizeAtom,
+ iconColorAtom,
filteredQueryResultsSelector,
isDarkThemeSelector,
-} from "../../state/selectors";
-import useGridSpans from "../../hooks/useGridSpans";
+} from "@/state";
+import Notice from "@/components/Notice";
+
+import DetailFooter from "./DetailFooter";
import IconGridItem from "./IconGridItem";
import TagCloud from "./TagCloud";
-import Notice from "../Notice/Notice";
import "./IconGrid.css";
const defaultSearchTags = [
@@ -26,13 +29,11 @@ const defaultSearchTags = [
type IconGridProps = {};
-const IconGrid: React.FC = () => {
+const IconGrid = (_: IconGridProps) => {
const weight = useRecoilValue(iconWeightAtom);
const size = useRecoilValue(iconSizeAtom);
const color = useRecoilValue(iconColorAtom);
const isDark = useRecoilValue(isDarkThemeSelector);
- const spans = useGridSpans();
-
const filteredQueryResults = useRecoilValue(filteredQueryResultsSelector);
const originOffset = useRef({ top: 0, left: 0 });
@@ -46,34 +47,26 @@ const IconGrid: React.FC = () => {
return (
Try searching a category or keyword:
-
+
);
return (
-
+
-
+
{filteredQueryResults.map((iconEntry, index) => (
))}
+
);
diff --git a/src/components/IconGrid/IconGridItem.tsx b/src/components/IconGrid/IconGridItem.tsx
index 9ef19f3..505f40d 100644
--- a/src/components/IconGrid/IconGridItem.tsx
+++ b/src/components/IconGrid/IconGridItem.tsx
@@ -1,19 +1,18 @@
-import React, {
+import {
useRef,
useLayoutEffect,
useEffect,
MutableRefObject,
+ HTMLAttributes,
} from "react";
import { useRecoilState } from "recoil";
-import { motion, AnimatePresence } from "framer-motion";
+import { motion } from "framer-motion";
-import { iconPreviewOpenAtom } from "../../state/atoms";
-import DetailsPanel from "./DetailsPanel";
-import { IconEntry } from "../../lib";
+import { IconEntry } from "@/lib";
+import { selectionEntryAtom } from "@/state";
-interface IconGridItemProps {
+interface IconGridItemProps extends HTMLAttributes
{
index: number;
- spans: number;
isDark: boolean;
entry: IconEntry;
originOffset: MutableRefObject<{ top: number; left: number }>;
@@ -31,18 +30,18 @@ const itemVariants = {
}),
};
-const IconGridItem: React.FC = (props) => {
- const { index, originOffset, entry } = props;
+const IconGridItem = (props: IconGridItemProps) => {
+ const { index, originOffset, entry, style } = props;
const { name, Icon } = entry;
- const [open, setOpen] = useRecoilState(iconPreviewOpenAtom);
- const isOpen = open === name;
+ const [selection, setSelectionEntry] = useRecoilState(selectionEntryAtom);
+ const isOpen = selection?.name === name;
const isNew = entry.tags.includes("*new*");
const isUpdated = entry.tags.includes("*updated*");
const delayRef = useRef(0);
const offset = useRef({ top: 0, left: 0 });
const ref = useRef();
- const handleOpen = () => setOpen(isOpen ? false : name);
+ const handleOpen = () => setSelectionEntry(isOpen ? null : entry);
// The measurement for all elements happens in the layoutEffect cycle
// This ensures that when we calculate distance in the effect cycle
@@ -76,8 +75,8 @@ const IconGridItem: React.FC = (props) => {
ref={ref}
tabIndex={0}
style={{
- order: index,
- backgroundColor: isOpen ? "rgba(163, 159, 171, 0.1)" : undefined,
+ ...style,
+ backgroundColor: isOpen ? "var(--translucent)" : undefined,
}}
custom={delayRef}
transition={transition}
@@ -92,9 +91,6 @@ const IconGridItem: React.FC = (props) => {
{isUpdated && • }
-
- {isOpen && }
-
>
);
};
diff --git a/src/components/IconGrid/TagCloud.css b/src/components/IconGrid/TagCloud.css
index 793edd6..57ee857 100644
--- a/src/components/IconGrid/TagCloud.css
+++ b/src/components/IconGrid/TagCloud.css
@@ -2,31 +2,27 @@
display: flex;
flex-wrap: wrap;
justify-content: center;
- padding: 24px;
}
button.tag-button {
margin: 4px;
border-radius: 4px;
- background-color: rgba(194, 186, 196, 0.25);
+ background-color: var(--sheer);
outline: none;
cursor: pointer;
transition: background-color 200ms ease, box-shadow 200ms ease;
+ color: var(--foreground);
}
button.tag-button:hover {
- background-color: rgba(194, 186, 196, 0.7);
+ background-color: var(--soft);
}
-button.tag-button:focus {
- box-shadow: 0 0 0 1px rgba(194, 186, 196, 0.7);
+button.tag-button:focus-visible {
+ box-shadow: 0 0 0 1px var(--soft);
}
.tag-button code {
padding: 4px;
font-size: 12px;
}
-
-.dark {
- color: white;
-}
diff --git a/src/components/IconGrid/TagCloud.tsx b/src/components/IconGrid/TagCloud.tsx
index 85121f8..a551594 100644
--- a/src/components/IconGrid/TagCloud.tsx
+++ b/src/components/IconGrid/TagCloud.tsx
@@ -1,23 +1,24 @@
-import React, { useCallback } from "react";
+import { useCallback } from "react";
import { useSetRecoilState } from "recoil";
-import { searchQueryAtom } from "../../state/atoms";
+import { useMediaQuery } from "@/hooks";
+import { searchQueryAtom } from "@/state";
import "./TagCloud.css";
interface TagCloudProps {
name: string;
tags: string[];
- isDark: boolean;
}
-const TagCloud: React.FC = ({ name, tags, isDark }) => {
+const TagCloud = ({ name, tags }: TagCloudProps) => {
+ const isMobile = useMediaQuery("(max-width: 719px)");
const setQuery = useSetRecoilState(searchQueryAtom);
const handleTagClick = useCallback(
(tag: string) => {
setQuery(tag);
- document.getElementById("search-input")?.focus();
+ !isMobile && document.getElementById("search-input")?.focus();
},
- [setQuery]
+ [setQuery, isMobile]
);
return (
@@ -28,7 +29,7 @@ const TagCloud: React.FC = ({ name, tags, isDark }) => {
className="tag-button"
onClick={() => void handleTagClick(tag)}
>
- {tag}
+ {tag}
{tag === "*new*" && • }
{tag === "*updated*" && • }
diff --git a/src/components/IconGrid/index.ts b/src/components/IconGrid/index.ts
new file mode 100644
index 0000000..4078c88
--- /dev/null
+++ b/src/components/IconGrid/index.ts
@@ -0,0 +1 @@
+export { default } from "./IconGrid";
diff --git a/src/components/Links/Links.css b/src/components/Links/Links.css
index fd241ef..710549d 100644
--- a/src/components/Links/Links.css
+++ b/src/components/Links/Links.css
@@ -23,6 +23,7 @@
a.nav-link {
text-decoration: none;
position: relative;
+ cursor: pointer;
color: black;
}
diff --git a/src/components/Links/Links.tsx b/src/components/Links/Links.tsx
index 8c8b41e..4d65524 100644
--- a/src/components/Links/Links.tsx
+++ b/src/components/Links/Links.tsx
@@ -1,21 +1,20 @@
-import React from "react";
-import { OutboundLink } from "react-ga";
-import { ArrowElbowDownRight } from "phosphor-react";
+import { ArrowElbowDownRight } from "@phosphor-icons/react";
-import { iconCount } from "../../lib/icons";
+import { iconCount } from "@/lib/icons";
+import OutboundLink from "@/components/OutboundLink";
import "./Links.css";
interface LinksProps {}
-const Links: React.FC = () => {
+const Links = (_: LinksProps) => {
return (
= () => {
Download all ({iconCount})
+
Figma library
{" / "}
plugin
+
+
- {/*
+
- */}
-
);
diff --git a/src/components/Links/index.ts b/src/components/Links/index.ts
new file mode 100644
index 0000000..4ca5192
--- /dev/null
+++ b/src/components/Links/index.ts
@@ -0,0 +1 @@
+export { default } from "./Links";
diff --git a/src/components/Notice/Notice.tsx b/src/components/Notice/Notice.tsx
index d54078b..d22cd76 100644
--- a/src/components/Notice/Notice.tsx
+++ b/src/components/Notice/Notice.tsx
@@ -1,26 +1,21 @@
-import React from "react";
+import { ReactNode } from "react";
import { motion } from "framer-motion";
import { useRecoilValue } from "recoil";
+import { HourglassMedium, Question, SmileyXEyes } from "@phosphor-icons/react";
-import { isDarkThemeSelector } from "../../state/selectors";
-import { searchQueryAtom } from "../../state/atoms";
-import { HourglassMedium, Question, SmileyXEyes } from "phosphor-react";
+import { searchQueryAtom } from "@/state";
interface NoticeProps {
message?: string;
type?: "wait" | "help" | "warn" | "none";
+ children?: ReactNode;
}
-const Notice: React.FC = ({
- message,
- type = "warn",
- children,
-}) => {
- const isDark = useRecoilValue(isDarkThemeSelector);
+const Notice = ({ message, type = "warn", children }: NoticeProps) => {
const query = useRecoilValue(searchQueryAtom);
return (
-
+
,
+ HTMLAnchorElement
+ > {
+ eventLabel: string;
+}
+
+const NEWTAB = "_blank";
+const MIDDLECLICK = 1;
+const DEFAULT_META: UaEventOptions = {
+ category: "Outbound",
+ action: "Click",
+};
+
+const OutboundLink = ({
+ eventLabel,
+ target,
+ href,
+ ...props
+}: OutboundLinkProps) => {
+ const handleClick: MouseEventHandler = useCallback(
+ (event) => {
+ const eventMeta = { ...DEFAULT_META, label: eventLabel };
+ const sameTarget = target !== NEWTAB;
+ const normalClick = !(
+ event.ctrlKey ||
+ event.shiftKey ||
+ event.metaKey ||
+ event.button === MIDDLECLICK
+ );
+
+ if (!!href && sameTarget && normalClick) {
+ event.preventDefault();
+ ReactGA.event(eventMeta);
+ window.location.href = href;
+ } else {
+ ReactGA.event(eventMeta);
+ }
+ },
+ [href, eventLabel]
+ );
+
+ return (
+
+ );
+};
+
+export default OutboundLink;
diff --git a/src/components/OutboundLink/index.ts b/src/components/OutboundLink/index.ts
new file mode 100644
index 0000000..b54bc9a
--- /dev/null
+++ b/src/components/OutboundLink/index.ts
@@ -0,0 +1 @@
+export { default } from "./OutboundLink";
diff --git a/src/components/SearchInput/SearchInput.css b/src/components/SearchInput/SearchInput.css
index 4091e23..9e96243 100644
--- a/src/components/SearchInput/SearchInput.css
+++ b/src/components/SearchInput/SearchInput.css
@@ -6,7 +6,7 @@
padding: 0 24px;
border-radius: 8px;
color: white;
- background-color: rgba(255, 255, 255, 0.05);
+ background-color: var(--scrim);
}
.search-bar:focus-within {
diff --git a/src/components/SearchInput/SearchInput.tsx b/src/components/SearchInput/SearchInput.tsx
index 25ec8d6..414d55f 100644
--- a/src/components/SearchInput/SearchInput.tsx
+++ b/src/components/SearchInput/SearchInput.tsx
@@ -1,11 +1,17 @@
-import React, { useState, useEffect, useRef, MutableRefObject } from "react";
+import {
+ useState,
+ useEffect,
+ useRef,
+ MutableRefObject,
+ ReactNode,
+} from "react";
import { useRecoilState } from "recoil";
import { useDebounce } from "react-use";
import { useHotkeys } from "react-hotkeys-hook";
-import { Command, MagnifyingGlass, X, HourglassHigh } from "phosphor-react";
-import ReactGA from "react-ga";
+import { Command, MagnifyingGlass, X, HourglassHigh } from "@phosphor-icons/react";
+import ReactGA from "react-ga4";
-import { searchQueryAtom } from "../../state/atoms";
+import { searchQueryAtom } from "@/state";
import "./SearchInput.css";
const apple = /iPhone|iPod|iPad|Macintosh|MacIntel|MacPPC/i;
@@ -16,7 +22,7 @@ const isMobile = mobile.test(window.navigator.userAgent);
type SearchInputProps = {};
-const SearchInput: React.FC = () => {
+const SearchInput = (_: SearchInputProps) => {
const [value, setValue] = useState("");
const [query, setQuery] = useRecoilState(searchQueryAtom);
const inputRef =
@@ -78,8 +84,8 @@ const SearchInput: React.FC = () => {
value={value}
placeholder="Search"
onChange={({ currentTarget }) => setValue(currentTarget.value)}
- onKeyPress={({ currentTarget, key }) =>
- key === "Enter" && currentTarget.blur()
+ onKeyDown={({ currentTarget, key }) =>
+ (key === "Enter" || key === "Escape") && currentTarget.blur()
}
/>
{!value && !isMobile && {isApple ? : "Ctrl + "}K }
@@ -94,7 +100,7 @@ const SearchInput: React.FC = () => {
);
};
-const Keys: React.FC<{}> = ({ children }) => (
+const Keys = ({ children }: { children?: ReactNode }) => (
{children}
);
diff --git a/src/components/SearchInput/index.ts b/src/components/SearchInput/index.ts
new file mode 100644
index 0000000..1a2fa40
--- /dev/null
+++ b/src/components/SearchInput/index.ts
@@ -0,0 +1 @@
+export { default } from "./SearchInput";
diff --git a/src/components/SettingsActions/SettingsActions.css b/src/components/SettingsActions/SettingsActions.css
index 5a801d3..da317c5 100644
--- a/src/components/SettingsActions/SettingsActions.css
+++ b/src/components/SettingsActions/SettingsActions.css
@@ -1,17 +1,13 @@
button.action-button {
- background-color: rgba(255, 255, 255, 0.05);
+ background-color: var(--scrim);
color: white;
padding: 8px;
border-radius: 8px;
cursor: pointer;
}
-button.action-button:hover {
- background-color: rgba(255, 255, 255, 0.1);
-}
-
button.action-button:active {
- background-color: rgba(255, 255, 255, 0.2);
+ background-color: var(--sheer);
}
@media screen and (max-width: 558px) {
diff --git a/src/components/SettingsActions/SettingsActions.tsx b/src/components/SettingsActions/SettingsActions.tsx
index f7ef981..69c89ef 100644
--- a/src/components/SettingsActions/SettingsActions.tsx
+++ b/src/components/SettingsActions/SettingsActions.tsx
@@ -1,12 +1,17 @@
-import React from "react";
-import { ArrowCounterClockwise, CheckCircle, Link } from "phosphor-react";
import { useRecoilValue, useResetRecoilState } from "recoil";
-import { iconWeightAtom, iconSizeAtom, iconColorAtom } from "../../state/atoms";
-import "./SettingsActions.css";
-import useTransientState from "../../hooks/useTransientState";
-import { resetSettingsSelector } from "../../state/selectors";
+import { ArrowCounterClockwise, CheckCircle, Link } from "@phosphor-icons/react";
-const SettingsActions: React.FC = () => {
+import { useTransientState } from "@/hooks";
+import {
+ iconWeightAtom,
+ iconSizeAtom,
+ iconColorAtom,
+ resetSettingsSelector,
+} from "@/state";
+
+import "./SettingsActions.css";
+
+const SettingsActions = () => {
const weight = useRecoilValue(iconWeightAtom);
const size = useRecoilValue(iconSizeAtom);
const color = useRecoilValue(iconColorAtom);
diff --git a/src/components/SettingsActions/index.ts b/src/components/SettingsActions/index.ts
new file mode 100644
index 0000000..1e0e12e
--- /dev/null
+++ b/src/components/SettingsActions/index.ts
@@ -0,0 +1 @@
+export { default } from "./SettingsActions";
diff --git a/src/components/SizeInput/SizeInput.css b/src/components/SizeInput/SizeInput.css
index 7529d38..d04a2a0 100644
--- a/src/components/SizeInput/SizeInput.css
+++ b/src/components/SizeInput/SizeInput.css
@@ -7,7 +7,7 @@
padding: 0 24px;
color: white;
border-radius: 8px;
- background-color: rgba(255, 255, 255, 0.05);
+ background-color: var(--scrim);
font-family: "Manrope", sans-serif;
font-size: 16px;
}
@@ -68,12 +68,12 @@
outline: none;
width: 24px; /* Set a specific slider handle width */
height: 24px; /* Slider handle height */
- box-shadow: 0 0 0 6px rgba(255, 255, 255, 0.2);
+ box-shadow: 0 0 0 6px var(--sheer);
}
.size-bar input:focus::-webkit-slider-thumb {
outline: none;
width: 24px; /* Set a specific slider handle width */
height: 24px; /* Slider handle height */
- box-shadow: 0 0 0 6px rgba(255, 255, 255, 0.2);
+ box-shadow: 0 0 0 6px var(--sheer);
}
diff --git a/src/components/SizeInput/SizeInput.tsx b/src/components/SizeInput/SizeInput.tsx
index d8e37be..7d8aaf9 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 { iconSizeAtom } from "../../state/atoms";
+import { iconSizeAtom } from "@/state";
import "./SizeInput.css";
type SizeInputProps = {};
@@ -14,7 +14,7 @@ const handleBlur = (event: React.UIEvent) => {
event.currentTarget.blur();
};
-const SizeInput: React.FC = () => {
+const SizeInput = (_: SizeInputProps) => {
const [size, setSize] = useRecoilState(iconSizeAtom);
const handleSizeChange = useCallback(
diff --git a/src/components/SizeInput/index.ts b/src/components/SizeInput/index.ts
new file mode 100644
index 0000000..eb5f6d1
--- /dev/null
+++ b/src/components/SizeInput/index.ts
@@ -0,0 +1 @@
+export { default } from "./SizeInput";
diff --git a/src/components/StyleInput/StyleInput.css b/src/components/StyleInput/StyleInput.css
index dd94d75..279205e 100644
--- a/src/components/StyleInput/StyleInput.css
+++ b/src/components/StyleInput/StyleInput.css
@@ -1,20 +1,3 @@
-/* .style-select {
- position: relative;
-}
-
-.style-select {
- background-color: gold;
- border-radius: 24px;
- box-shadow: 4px 4px #ccc;
- display: none;
-}
-
-.style-select option {
- background-color: gold;
- border-radius: 24px;
- display: none;
-} */
-
.react-dropdown-select {
width: 176px !important;
height: 48px !important;
@@ -22,7 +5,7 @@
padding: 0 24px !important;
color: white;
border-radius: 8px !important;
- background-color: rgba(255, 255, 255, 0.05);
+ background-color: var(--scrim);
font-size: 16px;
border: none !important;
}
@@ -50,19 +33,6 @@
box-shadow: none !important;
}
-/* .react-dropdown-select-type-single {
- height: 100% !important;
-} */
-
-/* .react-dropdown-select-clear,
-.react-dropdown-select-dropdown-handle {
- color: #fff;
-} */
-
-/* .react-dropdown-select-option {
- border: 1px solid #000;
-} */
-
.react-dropdown-select-item {
color: #333;
height: 40px !important;
@@ -89,25 +59,24 @@
max-height: 300px;
overflow: auto;
z-index: 9;
- /* background: rgb(29, 20, 20) !important; */
box-shadow: none;
}
.react-dropdown-select-item {
color: black;
}
.react-dropdown-select-item:hover {
- background-color: #ffd171 !important;
+ background-color: var(--yellow) !important;
}
.react-dropdown-select-item.react-dropdown-select-item-selected,
.react-dropdown-select-item.react-dropdown-select-item-active {
color: black !important;
- background-color: #ffd171 !important;
+ background-color: var(--yellow) !important;
}
.react-dropdown-select-item:focus {
color: black !important;
- background-color: #ffd171 !important;
+ background-color: var(--yellow) !important;
}
.react-dropdown-select-item.react-dropdown-select-item-disabled {
diff --git a/src/components/StyleInput/StyleInput.tsx b/src/components/StyleInput/StyleInput.tsx
index cd52a5c..c18c285 100644
--- a/src/components/StyleInput/StyleInput.tsx
+++ b/src/components/StyleInput/StyleInput.tsx
@@ -1,10 +1,11 @@
-import React, { useMemo } from "react";
+import { useMemo } from "react";
import { useRecoilState } from "recoil";
import Select from "react-dropdown-select";
-import { PencilLine } from "phosphor-react";
+import { PencilLine } from "@phosphor-icons/react";
import { IconStyle } from "@phosphor-icons/core";
-import { iconWeightAtom } from "../../state/atoms";
+import { iconWeightAtom } from "@/state";
+
import "./StyleInput.css";
type WeightOption = { key: string; value: IconStyle; icon: JSX.Element };
@@ -44,7 +45,7 @@ const options: WeightOption[] = [
type StyleInputProps = {};
-const StyleInput: React.FC = () => {
+const StyleInput = (_: StyleInputProps) => {
const [style, setStyle] = useRecoilState(iconWeightAtom);
const currentStyle = useMemo(
diff --git a/src/components/StyleInput/index.ts b/src/components/StyleInput/index.ts
new file mode 100644
index 0000000..4f7b186
--- /dev/null
+++ b/src/components/StyleInput/index.ts
@@ -0,0 +1 @@
+export { default } from "./StyleInput";
diff --git a/src/components/Tabs/Tabs.css b/src/components/Tabs/Tabs.css
new file mode 100644
index 0000000..1c9a591
--- /dev/null
+++ b/src/components/Tabs/Tabs.css
@@ -0,0 +1,56 @@
+.tabs {
+ display: flex;
+ flex-direction: column;
+}
+
+.tabs-header {
+ display: flex;
+ align-items: center;
+ gap: 4px;
+}
+
+button.tab {
+ all: unset;
+ padding: 6px 4px;
+ font-size: 12px;
+ text-align: center;
+ cursor: pointer;
+ flex: 1;
+ border-top-left-radius: 8px;
+ border-top-right-radius: 8px;
+ z-index: 2;
+}
+
+button.tab:focus-visible {
+ outline: 1px solid currentColor;
+}
+
+button.tab:hover:not(.active) {
+ background-color: var(--sheer);
+}
+
+button.tab.active {
+ background-color: var(--background);
+ border-bottom: none;
+}
+
+.tab-content {
+ flex: 1;
+ height: 77px;
+ max-height: 77px;
+ padding: 20px 20px 10px;
+ border-radius: 8px;
+ background-color: var(--background);
+ overflow-y: auto;
+}
+
+@media screen and (max-width: 719px) {
+ .tabs {
+ flex: 1;
+ }
+
+ .tab-content {
+ height: unset;
+ max-height: unset;
+ }
+}
diff --git a/src/components/Tabs/Tabs.tsx b/src/components/Tabs/Tabs.tsx
new file mode 100644
index 0000000..f524380
--- /dev/null
+++ b/src/components/Tabs/Tabs.tsx
@@ -0,0 +1,59 @@
+import { CSSProperties, ReactNode, useState } from "react";
+
+import "./Tabs.css";
+
+export type Tab = {
+ header: ReactNode;
+ content: ReactNode;
+};
+
+type TabsProps = {
+ tabs: Tab[];
+ initialIndex?: number;
+ onTabChange?: (index: number) => void;
+};
+
+const contentStyles: Record = {
+ activeLeft: { borderTopLeftRadius: 0 },
+ activeRight: { borderTopRightRadius: 0 },
+} as const;
+
+const Tabs = ({ tabs, initialIndex = 0, onTabChange }: TabsProps) => {
+ const [activeIndex, setActiveIndex] = useState(
+ !!tabs[initialIndex] ? initialIndex : 0
+ );
+
+ return (
+
+
+ {tabs.map((tab, i) => (
+ {
+ setActiveIndex(i);
+ onTabChange?.(i);
+ }}
+ >
+ {tab.header}
+
+ ))}
+
+
+ {tabs[activeIndex]?.content}
+
+
+ );
+};
+
+export default Tabs;
diff --git a/src/components/Tabs/index.ts b/src/components/Tabs/index.ts
new file mode 100644
index 0000000..18ff5c3
--- /dev/null
+++ b/src/components/Tabs/index.ts
@@ -0,0 +1,2 @@
+export { default } from "./Tabs";
+export type { Tab } from "./Tabs";
diff --git a/src/components/Toolbar/Toolbar.css b/src/components/Toolbar/Toolbar.css
index 1078f03..c9f5576 100644
--- a/src/components/Toolbar/Toolbar.css
+++ b/src/components/Toolbar/Toolbar.css
@@ -4,12 +4,12 @@ nav.toolbar {
top: -1px;
padding: 0;
margin: 0;
- background-color: #35313d;
- z-index: 1;
+ background-color: var(--eggplant);
+ z-index: 2;
display: flex;
justify-content: center;
align-items: center;
- box-shadow: 0 2px 0 0 rgba(0, 0, 0, 0.15);
+ box-shadow: 0 2px 0 0 var(--shadow);
}
.toolbar-contents {
diff --git a/src/components/Toolbar/Toolbar.tsx b/src/components/Toolbar/Toolbar.tsx
index ac3c17f..417b33c 100644
--- a/src/components/Toolbar/Toolbar.tsx
+++ b/src/components/Toolbar/Toolbar.tsx
@@ -1,11 +1,11 @@
import React from "react";
+import StyleInput from "@/components/StyleInput";
+import SearchInput from "@/components/SearchInput";
+import SizeInput from "@/components/SizeInput";
+import ColorInput from "@/components/ColorInput";
+import SettingsActions from "@/components/SettingsActions";
import "./Toolbar.css";
-import StyleInput from "../StyleInput/StyleInput";
-import SearchInput from "../SearchInput/SearchInput";
-import SizeInput from "../SizeInput/SizeInput";
-import ColorInput from "../ColorInput/ColorInput";
-import SettingsActions from "../SettingsActions/SettingsActions";
type ToolbarProps = {};
diff --git a/src/components/Toolbar/index.ts b/src/components/Toolbar/index.ts
new file mode 100644
index 0000000..cec4f38
--- /dev/null
+++ b/src/components/Toolbar/index.ts
@@ -0,0 +1 @@
+export { default } from "./Toolbar";
diff --git a/src/hooks/index.ts b/src/hooks/index.ts
new file mode 100644
index 0000000..eb8c161
--- /dev/null
+++ b/src/hooks/index.ts
@@ -0,0 +1,13 @@
+export { default as useCSSVariables } from "./useCSSVariables";
+export { default as useDebounce } from "./useDebounce";
+export { default as useEvent } from "./useEvent";
+export { default as useIconParameters } from "./useIconParameters";
+export { default as useLocalStorage } from "./useLocalStorage";
+export { default as useMediaQuery } from "./useMediaQuery";
+export { default as usePersistSettings } from "./usePersistSettings";
+export { default as useSessionStorage } from "./useSessionStorage";
+export { default as useThrottle } from "./useThrottle";
+export { default as useThrottled } from "./useThrottled";
+export { default as useTimeoutFn } from "./useTimeoutFn";
+export { default as useTransientState } from "./useTransientState";
+export { default as useUnmount } from "./useUnmount";
diff --git a/src/hooks/useCSSVariables.ts b/src/hooks/useCSSVariables.ts
new file mode 100644
index 0000000..787484f
--- /dev/null
+++ b/src/hooks/useCSSVariables.ts
@@ -0,0 +1,40 @@
+import { useEffect, useRef } from "react";
+
+type CSSCustomPropertyName = `--${string}`;
+
+type CSSCustomProperties = {
+ [property: CSSCustomPropertyName]: string | null;
+};
+
+function simpleDiff(prev: CSSCustomProperties, next: CSSCustomProperties) {
+ const merge = { ...prev, ...next };
+ return Object.entries(merge).reduce<
+ [property: CSSCustomPropertyName, value: string | null][]
+ >((acc, [k, val]) => {
+ let key = k as CSSCustomPropertyName;
+
+ if (
+ !prev[key as CSSCustomPropertyName] ||
+ prev[key as CSSCustomPropertyName] !== val
+ ) {
+ acc.push([key, val]);
+ }
+ return acc;
+ }, []);
+}
+
+export default function useCSSVariables(properties: CSSCustomProperties) {
+ const p = useRef({});
+
+ useEffect(() => {
+ const diff = simpleDiff(p.current, properties);
+
+ if (diff.length > 0) {
+ diff.forEach(([key, value]) => {
+ document.documentElement.style.setProperty(key, value);
+ });
+
+ p.current = properties;
+ }
+ }, [properties]);
+}
diff --git a/src/hooks/useEvent.ts b/src/hooks/useEvent.ts
new file mode 100644
index 0000000..251214b
--- /dev/null
+++ b/src/hooks/useEvent.ts
@@ -0,0 +1,45 @@
+import { useEffect } from "react";
+
+export type UseEventTarget = HTMLElement | SVGElement | Document | Window;
+
+export type UseEventMap = E extends HTMLElement
+ ? HTMLElementEventMap
+ : E extends SVGElement
+ ? SVGElementEventMap
+ : E extends Document
+ ? DocumentEventMap
+ : WindowEventMap;
+
+export type UseEventType = keyof UseEventMap;
+
+/**
+ * Attach event listeners to arbitrary targets, and perform necessary cleanup
+ * when unmounting. Provides type inference for the listener based on the
+ * provided event name (currently supports {@link Window}, {@link Document},
+ * and subclasses of {@link HTMLElement} and {@link SVGElement}).
+ *
+ * @param type an {@link https://developer.mozilla.org/en-US/docs/Web/Events#event_listing event type}
+ * @param listener a callback to be fired on the event
+ * @param options {@link AddEventListenerOptions}
+ * @param el the target element to attack the listener. Defaults to
+ * {@link Document} when omitted.
+ */
+export default function useEvent<
+ K extends UseEventType,
+ M extends UseEventMap,
+ T extends UseEventTarget = Document
+>(
+ type: K,
+ listener: (this: T, ev: M[K]) => any,
+ options?: boolean | AddEventListenerOptions,
+ el?: T
+) {
+ useEffect(() => {
+ const target = el ?? document;
+ // @ts-ignore
+ target.addEventListener(type, listener, options);
+
+ // @ts-ignore
+ return () => target.removeEventListener(type, listener);
+ }, [el, type]);
+}
diff --git a/src/hooks/useGridSpans.ts b/src/hooks/useGridSpans.ts
deleted file mode 100644
index e65e604..0000000
--- a/src/hooks/useGridSpans.ts
+++ /dev/null
@@ -1,18 +0,0 @@
-import { useWindowSize } from "react-use";
-
-const MOBILE_BREAKPOINT = 536;
-
-const GRID_PADDING = 32; // .grid-container { padding }
-const TOOLBAR_WIDTH = 17; // IS THIS BROWSER-SPECIFIC?
-const MAX_GRID_WIDTH = 1120; // .grid { max-width }
-const ITEM_WIDTH = 168; // .grid-item { width; height; margin }
-const ITEM_WIDTH_MOBILE = 108; // .grid-item { width; height; margin }
-
-export default (): number => {
- const { width } = useWindowSize();
- const itemWidth = width <= MOBILE_BREAKPOINT ? ITEM_WIDTH_MOBILE : ITEM_WIDTH;
-
- return Math.floor(
- Math.min(width - GRID_PADDING - TOOLBAR_WIDTH, MAX_GRID_WIDTH) / itemWidth
- );
-};
diff --git a/src/hooks/useLocalStorage.ts b/src/hooks/useLocalStorage.ts
new file mode 100644
index 0000000..4d49cbb
--- /dev/null
+++ b/src/hooks/useLocalStorage.ts
@@ -0,0 +1,40 @@
+import { useCallback, useState, Dispatch, SetStateAction } from "react";
+import { STORAGE_KEY } from "@/state";
+
+type Initializer = () => S;
+type Setter = (prev: S) => S;
+type Action = S | Setter | Initializer;
+
+function expand(action: Action, prev?: S) {
+ if (typeof action === "function") {
+ return (action as Setter)(prev!);
+ } else {
+ return action;
+ }
+}
+
+export default function useLocalStorage(
+ key: string,
+ fallbackState: S | (() => S)
+): [S, Dispatch>, (partial: Partial) => void] {
+ const [value, setValue] = useState(() => {
+ let val = localStorage.getItem(STORAGE_KEY + key);
+ if (val) return JSON.parse(val) as S;
+ return expand(fallbackState);
+ });
+
+ const set: Dispatch> = useCallback((val) => {
+ setValue((prev) => {
+ const next = expand(val, prev);
+ localStorage.setItem(STORAGE_KEY + key, JSON.stringify(next));
+ return next;
+ });
+ }, []);
+
+ const insert = useCallback(
+ (partial: Partial) => set((value) => ({ ...value, ...partial })),
+ []
+ );
+
+ return [value, set, insert];
+}
diff --git a/src/hooks/useMediaQuery.ts b/src/hooks/useMediaQuery.ts
new file mode 100644
index 0000000..0e8a5c9
--- /dev/null
+++ b/src/hooks/useMediaQuery.ts
@@ -0,0 +1,12 @@
+import { useMemo, useReducer, Reducer } from "react";
+import useEvent from "./useEvent";
+
+const updater: Reducer = (s) => (s + 1) % 1_000_000;
+
+export default function useMediaQuery(query: string) {
+ const mq = useMemo(() => window.matchMedia(query), [query]);
+ const [, update] = useReducer(updater, 0);
+
+ useEvent("resize", update, { passive: true });
+ return mq.matches;
+}
diff --git a/src/hooks/usePersistSettings.ts b/src/hooks/usePersistSettings.ts
index 689c01d..13a6562 100644
--- a/src/hooks/usePersistSettings.ts
+++ b/src/hooks/usePersistSettings.ts
@@ -1,6 +1,11 @@
import { useRecoilValue } from "recoil";
+import {
+ iconWeightAtom,
+ iconSizeAtom,
+ iconColorAtom,
+ STORAGE_KEY,
+} from "@/state";
import useDebounce from "./useDebounce";
-import { iconWeightAtom, iconSizeAtom, iconColorAtom } from "../state/atoms";
export default function usePersistSettings() {
const weight = useRecoilValue(iconWeightAtom);
@@ -10,7 +15,7 @@ export default function usePersistSettings() {
useDebounce(
() => {
const serializedState = JSON.stringify({ weight, size, color });
- window.localStorage.setItem("__phosphor_settings__", serializedState);
+ window.localStorage.setItem(STORAGE_KEY, serializedState);
},
2000,
[weight, size, color]
diff --git a/src/hooks/useSessionStorage.ts b/src/hooks/useSessionStorage.ts
new file mode 100644
index 0000000..9ce2453
--- /dev/null
+++ b/src/hooks/useSessionStorage.ts
@@ -0,0 +1,40 @@
+import { useCallback, useState, Dispatch, SetStateAction } from "react";
+import { STORAGE_KEY } from "@/state";
+
+type Initializer = () => S;
+type Setter = (prev: S) => S;
+type Action = S | Setter | Initializer;
+
+function expand(action: Action, prev?: S) {
+ if (typeof action === "function") {
+ return (action as Setter)(prev!);
+ } else {
+ return action;
+ }
+}
+
+export default function useSessionStorage(
+ key: string,
+ fallbackState: S | (() => S)
+): [S, Dispatch>, (partial: Partial) => void] {
+ const [value, setValue] = useState(() => {
+ let val = sessionStorage.getItem(STORAGE_KEY + key);
+ if (val) return JSON.parse(val) as S;
+ return expand(fallbackState);
+ });
+
+ const set: Dispatch> = useCallback((val) => {
+ setValue((prev) => {
+ const next = expand(val, prev);
+ sessionStorage.setItem(STORAGE_KEY + key, JSON.stringify(next));
+ return next;
+ });
+ }, []);
+
+ const insert = useCallback(
+ (partial: Partial) => set((value) => ({ ...value, ...partial })),
+ []
+ );
+
+ return [value, set, insert];
+}
diff --git a/src/index.tsx b/src/index.tsx
index 9a0e59d..050a569 100644
--- a/src/index.tsx
+++ b/src/index.tsx
@@ -1,27 +1,23 @@
-import React from "react";
-import ReactDOM from "react-dom";
+import { StrictMode } from "react";
+import { createRoot } from "react-dom/client";
import { RecoilRoot } from "recoil";
-import * as serviceWorker from "./serviceWorker";
-import App from "./components/App/App";
-import ReactGA from "react-ga";
+import App from "./components/App";
+import ReactGA from "react-ga4";
-ReactGA.initialize("UA-179205759-1", { titleCase: false });
-ReactGA.pageview(window.location.pathname);
+const GA_MEASUREMENT_ID = 'G-1C1REQCLFB'
+ReactGA.initialize(GA_MEASUREMENT_ID);
-ReactDOM.render(
-
+const container = document.getElementById("root");
+const root = createRoot(container!);
+
+root.render(
+
- ,
- document.getElementById("root")
+
);
-// If you want your app to work offline and load faster, you can change
-// unregister() to register() below. Note this comes with some pitfalls.
-// Learn more about service workers: https://bit.ly/CRA-PWA
-serviceWorker.unregister();
-
console.log(`
%c sphorphosphor %co%cspho
diff --git a/src/lib/icons.ts b/src/lib/icons.ts
index 92639b3..99afcb1 100644
--- a/src/lib/icons.ts
+++ b/src/lib/icons.ts
@@ -1,12 +1,10 @@
-import * as Icons from "phosphor-react";
+import * as Icons from "@phosphor-icons/react";
import { icons as iconData } from "@phosphor-icons/core";
import { IconEntry } from ".";
export const icons: ReadonlyArray = iconData.map((entry) => ({
- name: entry.name,
- categories: entry.categories,
- tags: entry.tags,
+ ...entry,
Icon: Icons[entry.pascal_name as keyof typeof Icons] as Icons.Icon,
}));
@@ -14,6 +12,4 @@ if (process.env.NODE_ENV === "development") {
console.log(`${icons.length} icons`);
}
-export const iconCount = (icons.length * 6)
- .toString()
- .replace(/\B(?=(\d{3})+(?!\d))/g, ",");
+export const iconCount = Intl.NumberFormat("en-US").format(icons.length * 6);
diff --git a/src/lib/index.ts b/src/lib/index.ts
index fece71e..921793a 100644
--- a/src/lib/index.ts
+++ b/src/lib/index.ts
@@ -1,10 +1,7 @@
-import { Icon } from "phosphor-react";
-import { IconCategory } from "@phosphor-icons/core";
+import { Icon } from "@phosphor-icons/react";
+import { IconEntry as CoreEntry } from "@phosphor-icons/core";
-export interface IconEntry {
- name: string;
- categories: IconCategory[];
- tags: string[];
+export interface IconEntry extends CoreEntry {
Icon: Icon;
}
@@ -13,4 +10,5 @@ export enum SnippetType {
VUE = "Vue",
HTML = "HTML/CSS",
FLUTTER = "Flutter",
+ ELM = "Elm",
}
diff --git a/src/react-app-env.d.ts b/src/react-app-env.d.ts
deleted file mode 100644
index 6431bc5..0000000
--- a/src/react-app-env.d.ts
+++ /dev/null
@@ -1 +0,0 @@
-///
diff --git a/src/serviceWorker.ts b/src/serviceWorker.ts
deleted file mode 100644
index e7b8199..0000000
--- a/src/serviceWorker.ts
+++ /dev/null
@@ -1,146 +0,0 @@
-// This optional code is used to register a service worker.
-// register() is not called by default.
-
-// This lets the app load faster on subsequent visits in production, and gives
-// it offline capabilities. However, it also means that developers (and users)
-// will only see deployed updates on subsequent visits to a page, after all the
-// existing tabs open on the page have been closed, since previously cached
-// resources are updated in the background.
-
-// To learn more about the benefits of this model and instructions on how to
-// opt-in, read https://bit.ly/CRA-PWA
-
-const isLocalhost = Boolean(
- window.location.hostname === "localhost" ||
- // [::1] is the IPv6 localhost address.
- window.location.hostname === "[::1]" ||
- // 127.0.0.0/8 are considered localhost for IPv4.
- window.location.hostname.match(
- /^127(?:\.(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)){3}$/
- )
-);
-
-type Config = {
- onSuccess?: (registration: ServiceWorkerRegistration) => void;
- onUpdate?: (registration: ServiceWorkerRegistration) => void;
-};
-
-export function register(config?: Config) {
- if (process.env.NODE_ENV === "production" && "serviceWorker" in navigator) {
- // The URL constructor is available in all browsers that support SW.
- const publicUrl = new URL(process.env.PUBLIC_URL, window.location.href);
- if (publicUrl.origin !== window.location.origin) {
- // Our service worker won't work if PUBLIC_URL is on a different origin
- // from what our page is served on. This might happen if a CDN is used to
- // serve assets; see https://github.com/facebook/create-react-app/issues/2374
- return;
- }
-
- window.addEventListener("load", () => {
- const swUrl = `${process.env.PUBLIC_URL}/service-worker.js`;
-
- if (isLocalhost) {
- // This is running on localhost. Let's check if a service worker still exists or not.
- checkValidServiceWorker(swUrl, config);
-
- // Add some additional logging to localhost, pointing developers to the
- // service worker/PWA documentation.
- navigator.serviceWorker.ready.then(() => {
- console.log(
- "This web app is being served cache-first by a service " +
- "worker. To learn more, visit https://bit.ly/CRA-PWA"
- );
- });
- } else {
- // Is not localhost. Just register service worker
- registerValidSW(swUrl, config);
- }
- });
- }
-}
-
-function registerValidSW(swUrl: string, config?: Config) {
- navigator.serviceWorker
- .register(swUrl)
- .then((registration) => {
- registration.onupdatefound = () => {
- const installingWorker = registration.installing;
- if (installingWorker == null) {
- return;
- }
- installingWorker.onstatechange = () => {
- if (installingWorker.state === "installed") {
- if (navigator.serviceWorker.controller) {
- // At this point, the updated precached content has been fetched,
- // but the previous service worker will still serve the older
- // content until all client tabs are closed.
- console.log(
- "New content is available and will be used when all " +
- "tabs for this page are closed. See https://bit.ly/CRA-PWA."
- );
-
- // Execute callback
- if (config && config.onUpdate) {
- config.onUpdate(registration);
- }
- } else {
- // At this point, everything has been precached.
- // It's the perfect time to display a
- // "Content is cached for offline use." message.
- console.log("Content is cached for offline use.");
-
- // Execute callback
- if (config && config.onSuccess) {
- config.onSuccess(registration);
- }
- }
- }
- };
- };
- })
- .catch((error) => {
- console.error("Error during service worker registration:", error);
- });
-}
-
-function checkValidServiceWorker(swUrl: string, config?: Config) {
- // Check if the service worker can be found. If it can't reload the page.
- fetch(swUrl, {
- headers: { "Service-Worker": "script" },
- })
- .then((response) => {
- // Ensure service worker exists, and that we really are getting a JS file.
- const contentType = response.headers.get("content-type");
- if (
- response.status === 404 ||
- (contentType != null && contentType.indexOf("javascript") === -1)
- ) {
- // No service worker found. Probably a different app. Reload the page.
- navigator.serviceWorker.ready.then((registration) => {
- registration.unregister().then(() => {
- window.location.reload();
- });
- });
- } else {
- // Service worker found. Proceed as normal.
- registerValidSW(swUrl, config);
- }
- })
- .catch(() => {
- console.log(
- "No internet connection found. App is running in offline mode."
- );
- });
-}
-
-export function unregister() {
- if ("serviceWorker" in navigator) {
- navigator.serviceWorker.ready
- .then((registration) => {
- registration.unregister();
- })
- .catch((error) => {
- console.error(error.message);
- });
- }
-}
diff --git a/src/setupTests.ts b/src/setupTests.ts
deleted file mode 100644
index 5fdf001..0000000
--- a/src/setupTests.ts
+++ /dev/null
@@ -1,5 +0,0 @@
-// jest-dom adds custom jest matchers for asserting on DOM nodes.
-// allows you to do things like:
-// expect(element).toHaveTextContent(/react/i)
-// learn more: https://github.com/testing-library/jest-dom
-import "@testing-library/jest-dom/extend-expect";
diff --git a/src/state/atoms.ts b/src/state/atoms.ts
index 2afb5c8..b6d570f 100644
--- a/src/state/atoms.ts
+++ b/src/state/atoms.ts
@@ -1,27 +1,33 @@
import { atom } from "recoil";
import { IconStyle } from "@phosphor-icons/core";
+import { IconEntry } from "@/lib";
export const searchQueryAtom = atom({
- key: "searchQueryAtom",
+ key: "searchQuery",
default: "",
});
export const iconWeightAtom = atom({
- key: "iconWeightAtom",
+ key: "iconWeight",
default: IconStyle.REGULAR,
});
export const iconSizeAtom = atom({
- key: "iconSizeAtom",
+ key: "iconSize",
default: 32,
});
export const iconColorAtom = atom({
- key: "iconColorAtom",
+ key: "iconColor",
default: "#000000",
});
export const iconPreviewOpenAtom = atom({
- key: "iconPreviewOpenAtom",
+ key: "iconPreviewOpen",
default: false,
});
+
+export const selectionEntryAtom = atom({
+ key: "selectionEntry",
+ default: null,
+});
diff --git a/src/state/index.ts b/src/state/index.ts
new file mode 100644
index 0000000..f9c6a4c
--- /dev/null
+++ b/src/state/index.ts
@@ -0,0 +1,4 @@
+export * from "./atoms";
+export * from "./selectors";
+
+export const STORAGE_KEY = "__phosphor_settings__";
diff --git a/src/state/selectors.ts b/src/state/selectors.ts
index 157aa2a..d76514a 100644
--- a/src/state/selectors.ts
+++ b/src/state/selectors.ts
@@ -9,8 +9,8 @@ import {
iconSizeAtom,
iconColorAtom,
} from "./atoms";
-import { IconEntry } from "../lib";
-import { icons } from "../lib/icons";
+import { IconEntry } from "@/lib";
+import { icons } from "@/lib/icons";
const fuse = new Fuse(icons, {
keys: [{ name: "name", weight: 4 }, "tags", "categories"],
@@ -20,7 +20,7 @@ const fuse = new Fuse(icons, {
});
export const filteredQueryResultsSelector = selector>({
- key: "filteredQueryResultsSelector",
+ key: "filteredQueryResults",
get: ({ get }) => {
const query = get(searchQueryAtom).trim().toLowerCase();
if (!query) return icons;
@@ -36,7 +36,7 @@ type CategorizedIcons = Partial>;
export const categorizedQueryResultsSelector = selector<
Readonly
>({
- key: "categorizedQueryResultsSelector",
+ key: "categorizedQueryResults",
get: ({ get }) => {
const filteredResults = get(filteredQueryResultsSelector);
return new Promise((resolve) =>
@@ -57,7 +57,7 @@ export const singleCategoryQueryResultsSelector = selectorFamily<
ReadonlyArray,
IconCategory
>({
- key: "singleCategoryQueryResultsSelector",
+ key: "singleCategoryQueryResults",
get:
(category: IconCategory) =>
({ get }) => {
@@ -71,7 +71,7 @@ export const singleCategoryQueryResultsSelector = selectorFamily<
});
export const isDarkThemeSelector = selector({
- key: "isDarkThemeSelector",
+ key: "isDarkTheme",
get: ({ get }) => TinyColor(get(iconColorAtom)).isLight(),
});
diff --git a/src/utils/index.ts b/src/utils/index.ts
index 60b3979..bb28914 100644
--- a/src/utils/index.ts
+++ b/src/utils/index.ts
@@ -1,6 +1,6 @@
import { IconStyle } from "@phosphor-icons/core";
-import { SnippetType } from "../lib";
+import { SnippetType } from "@/lib";
export function getCodeSnippets({
name,
@@ -17,11 +17,13 @@ export function getCodeSnippets({
}): Record {
const isDefaultWeight = weight === "regular";
const isDefaultColor = color === "#000000";
+ const elmName = displayName.replace(/^\w/, (c) => c.toLowerCase());
+ const elmWeight = weight.replace(/^\w/, (c) => c.toUpperCase());
return {
- [SnippetType.HTML]: ` `,
+ } ph-${name}">`,
[SnippetType.REACT]: `<${displayName} size={${size}} ${
!isDefaultColor ? `color="${color}" ` : ""
}${isDefaultWeight ? "" : `weight="${weight}" `}/>`,
@@ -38,6 +40,12 @@ export function getCodeSnippets({
},\n size: ${size.toFixed(1)},\n${
!isDefaultColor ? ` color: Color(0xff${color.replace("#", "")}),\n` : ""
})`,
+ [SnippetType.ELM]: `Phosphor.${elmName}${
+ isDefaultWeight ? "" : " " + elmWeight
+ }
+ |> withSize ${size}
+ |> withSizeUnit "px"
+ |> toHtml []`,
};
}
@@ -48,6 +56,6 @@ export function supportsWeight({
type: SnippetType;
weight: IconStyle;
}): boolean {
- if (type !== SnippetType.HTML && type !== SnippetType.FLUTTER) return true;
+ if (type !== SnippetType.FLUTTER) return true;
return weight !== IconStyle.DUOTONE;
}
diff --git a/src/vite-env.d.ts b/src/vite-env.d.ts
new file mode 100644
index 0000000..d816124
--- /dev/null
+++ b/src/vite-env.d.ts
@@ -0,0 +1 @@
+///
diff --git a/tsconfig.json b/tsconfig.json
index cf0f8ac..4cf3ce0 100644
--- a/tsconfig.json
+++ b/tsconfig.json
@@ -1,23 +1,22 @@
{
"compilerOptions": {
- "target": "es5",
- "lib": [
- "es6",
- "dom",
- "dom.iterable",
- "esnext"
- ],
+ "baseUrl": "./",
+ "paths": {
+ "@/*": ["./src/*"]
+ },
+ "lib": ["DOM", "DOM.Iterable", "ESNext"],
+ "target": "ESNext",
"allowJs": true,
"skipLibCheck": true,
"esModuleInterop": true,
"allowSyntheticDefaultImports": true,
"strict": true,
"forceConsistentCasingInFileNames": true,
- "module": "esnext",
+ "module": "ESNext",
"moduleResolution": "node",
"resolveJsonModule": true,
"isolatedModules": true,
- "jsx": "react",
+ "jsx": "react-jsx",
"sourceMap": true,
"declaration": true,
"noUnusedLocals": true,
@@ -26,11 +25,6 @@
"noFallthroughCasesInSwitch": true,
"noEmit": true
},
- "include": [
- "src"
- ],
- "exclude": [
- "node_modules",
- "build"
- ]
+ "include": ["src"],
+ "exclude": ["node_modules", "build"]
}
diff --git a/vite.config.ts b/vite.config.ts
new file mode 100644
index 0000000..f224d2b
--- /dev/null
+++ b/vite.config.ts
@@ -0,0 +1,15 @@
+import path from "path";
+import { defineConfig } from "vite";
+import react from "@vitejs/plugin-react";
+import svgr from 'vite-plugin-svgr'
+
+// https://vitejs.dev/config/
+export default defineConfig({
+ plugins: [react(), svgr()],
+ resolve: {
+ alias: {
+ "~": path.resolve(__dirname, "./public"),
+ "@": path.resolve(__dirname, "./src"),
+ },
+ },
+});