From 1b8d6c48fcddb586ca76ac129ebcb62b59955f1f Mon Sep 17 00:00:00 2001 From: rektdeckard Date: Thu, 2 Feb 2023 22:15:00 -0700 Subject: [PATCH 1/8] chore(build): move to react 18 + vite --- .gitignore | 1 + bin/fetch.js | 93 ----------- public/index.html => index.html | 11 +- package.json | 50 +++--- src/components/App/App.test.tsx | 9 -- src/components/App/index.ts | 1 + src/components/Banner/Banner.tsx | 1 - src/components/Banner/index.ts | 1 + src/components/ColorInput/ColorInput.tsx | 11 +- src/components/ColorInput/index.ts | 1 + .../ErrorBoundary/ErrorBoundary.tsx | 9 +- src/components/ErrorBoundary/index.ts | 1 + src/components/Footer/Footer.tsx | 18 +-- src/components/Footer/index.ts | 1 + src/components/Header/Header.tsx | 78 ++++------ src/components/Header/index.ts | 1 + src/components/IconGrid/DetailsPanel.tsx | 17 +- src/components/IconGrid/IconGrid.tsx | 13 +- src/components/IconGrid/IconGridItem.tsx | 14 +- src/components/IconGrid/TagCloud.tsx | 6 +- src/components/IconGrid/index.ts | 1 + src/components/Links/Links.tsx | 5 +- src/components/Links/index.ts | 1 + src/components/Notice/Notice.tsx | 15 +- src/components/Notice/index.ts | 1 + src/components/SearchInput/SearchInput.tsx | 14 +- src/components/SearchInput/index.ts | 1 + .../SettingsActions/SettingsActions.tsx | 13 +- src/components/SettingsActions/index.ts | 1 + src/components/SizeInput/SizeInput.tsx | 4 +- src/components/SizeInput/index.ts | 1 + src/components/StyleInput/StyleInput.tsx | 7 +- src/components/StyleInput/index.ts | 1 + src/components/Toolbar/Toolbar.tsx | 10 +- src/components/Toolbar/index.ts | 1 + src/index.tsx | 22 ++- src/lib/icons.ts | 4 +- src/lib/index.ts | 7 +- src/react-app-env.d.ts | 1 - src/serviceWorker.ts | 146 ------------------ src/setupTests.ts | 5 - src/state/selectors.ts | 4 +- src/utils/index.ts | 2 +- src/vite-env.d.ts | 1 + tsconfig.json | 26 ++-- vite.config.ts | 15 ++ 46 files changed, 197 insertions(+), 449 deletions(-) delete mode 100644 bin/fetch.js rename public/index.html => index.html (92%) delete mode 100644 src/components/App/App.test.tsx create mode 100644 src/components/App/index.ts create mode 100644 src/components/Banner/index.ts create mode 100644 src/components/ColorInput/index.ts create mode 100644 src/components/ErrorBoundary/index.ts create mode 100644 src/components/Footer/index.ts create mode 100644 src/components/Header/index.ts create mode 100644 src/components/IconGrid/index.ts create mode 100644 src/components/Links/index.ts create mode 100644 src/components/Notice/index.ts create mode 100644 src/components/SearchInput/index.ts create mode 100644 src/components/SettingsActions/index.ts create mode 100644 src/components/SizeInput/index.ts create mode 100644 src/components/StyleInput/index.ts create mode 100644 src/components/Toolbar/index.ts delete mode 100644 src/react-app-env.d.ts delete mode 100644 src/serviceWorker.ts delete mode 100644 src/setupTests.ts create mode 100644 src/vite-env.d.ts create mode 100644 vite.config.ts diff --git a/.gitignore b/.gitignore index 7d3cec2e..9866ef4d 100644 --- a/.gitignore +++ b/.gitignore @@ -10,6 +10,7 @@ # production /build +/dist # misc .DS_Store diff --git a/bin/fetch.js b/bin/fetch.js deleted file mode 100644 index bb31925d..00000000 --- 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 7fd8915d..e67608fe 100644 --- a/public/index.html +++ b/index.html @@ -3,7 +3,7 @@ Phosphor Icons - + - + - +
+ diff --git a/package.json b/package.json index 74bc1677..1fecf57e 100644 --- a/package.json +++ b/package.json @@ -21,46 +21,40 @@ "repository": "github:phosphor-icons/phosphor-home", "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": "^1.4.7", "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", + "phosphor-react": "^1.4.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-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", + "tinycolor": "^0.0.1", "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/file-saver": "^2.0.5", + "@types/node": "^18.11.18", + "@types/react": "^18.0.27", + "@types/react-dom": "^18.0.10", "@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/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/components/App/App.test.tsx b/src/components/App/App.test.tsx deleted file mode 100644 index 352d7b8f..00000000 --- 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/index.ts b/src/components/App/index.ts new file mode 100644 index 00000000..8ce017e6 --- /dev/null +++ b/src/components/App/index.ts @@ -0,0 +1 @@ +export { default } from "./App"; diff --git a/src/components/Banner/Banner.tsx b/src/components/Banner/Banner.tsx index af87e198..66e2628d 100644 --- a/src/components/Banner/Banner.tsx +++ b/src/components/Banner/Banner.tsx @@ -1,4 +1,3 @@ -import React from "react"; import { Medal } from "phosphor-react"; import ReactGA from "react-ga"; diff --git a/src/components/Banner/index.ts b/src/components/Banner/index.ts new file mode 100644 index 00000000..907b6e02 --- /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 b98df2ae..f091dbbe 100644 --- a/src/components/ColorInput/ColorInput.tsx +++ b/src/components/ColorInput/ColorInput.tsx @@ -1,14 +1,15 @@ -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 { iconColorAtom } from "@/state/atoms"; +import { isDarkThemeSelector } from "@/state/selectors"; +import useThrottled from "@/hooks/useThrottled"; + 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 00000000..ea57087f --- /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 40bdf9bc..49229817 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 00000000..3406bab8 --- /dev/null +++ b/src/components/ErrorBoundary/index.ts @@ -0,0 +1 @@ +export { default } from "./ErrorBoundary"; diff --git a/src/components/Footer/Footer.tsx b/src/components/Footer/Footer.tsx index 3112c44d..89fe7b1a 100644 --- a/src/components/Footer/Footer.tsx +++ b/src/components/Footer/Footer.tsx @@ -1,15 +1,15 @@ -import React from "react"; import { Coffee, Heart } from "phosphor-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 UArrowUpLeft } from "@/assets/u-arrow-up-left.svg"; +import { ReactComponent as MarkerGreen } from "@/assets/marker-green.svg"; +import { ReactComponent as PostIt } from "@/assets/footer-mobile.svg"; import "./Footer.css"; type FooterProps = {}; -const Footer: React.FC = () => { +const Footer = (_: FooterProps) => { return (
@@ -23,7 +23,7 @@ const Footer: React.FC = () => { ?.scrollIntoView({ behavior: "smooth", block: "start" }); }} > - +
@@ -108,11 +108,11 @@ const Footer: React.FC = () => { {" "} by Mikhail Sharanda.

- +
- +
diff --git a/src/components/Footer/index.ts b/src/components/Footer/index.ts new file mode 100644 index 00000000..3738288b --- /dev/null +++ b/src/components/Footer/index.ts @@ -0,0 +1 @@ +export { default } from "./Footer"; diff --git a/src/components/Header/Header.tsx b/src/components/Header/Header.tsx index fd2cce6d..a1ecd726 100644 --- a/src/components/Header/Header.tsx +++ b/src/components/Header/Header.tsx @@ -1,22 +1,22 @@ -import React from "react"; import { ArrowCircleUpRight, ArrowCircleDown } from "phosphor-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 { 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 = {}; @@ -33,25 +33,21 @@ const handleScrollToIcons = () => .getElementById("toolbar") ?.scrollIntoView({ behavior: "smooth", block: "start" }); -const Header: React.FC = () => { +const Header = (_: HeaderProps) => { return (
- - - - - - - + + + + + + + - - + +

@@ -73,20 +69,12 @@ const Header: React.FC = () => {

- - - - - - + + + + + +
diff --git a/src/components/Header/index.ts b/src/components/Header/index.ts new file mode 100644 index 00000000..2764567d --- /dev/null +++ b/src/components/Header/index.ts @@ -0,0 +1 @@ +export { default } from "./Header"; diff --git a/src/components/IconGrid/DetailsPanel.tsx b/src/components/IconGrid/DetailsPanel.tsx index 8d2c88ab..642e7deb 100644 --- a/src/components/IconGrid/DetailsPanel.tsx +++ b/src/components/IconGrid/DetailsPanel.tsx @@ -1,4 +1,4 @@ -import React, { useRef, useEffect } from "react"; +import React, { useRef, useEffect, CSSProperties } from "react"; import { useRecoilValue, useSetRecoilState } from "recoil"; import { useHotkeys } from "react-hotkeys-hook"; import { motion } from "framer-motion"; @@ -12,11 +12,12 @@ import { iconSizeAtom, iconColorAtom, iconPreviewOpenAtom, -} from "../../state/atoms"; -import useTransientState from "../../hooks/useTransientState"; +} from "@/state/atoms"; +import useTransientState from "@/hooks/useTransientState"; +import { IconEntry, SnippetType } from "@/lib"; +import { getCodeSnippets, supportsWeight } from "@/utils"; + import TagCloud from "./TagCloud"; -import { IconEntry, SnippetType } from "../../lib"; -import { getCodeSnippets, supportsWeight } from "../../utils"; const panelVariants = { open: { @@ -58,7 +59,7 @@ const renderedSnippets = [ SnippetType.FLUTTER, ]; -const DetailsPanel: React.FC = (props) => { +const DetailsPanel = (props: InfoPanelProps) => { const { index, spans, isDark, entry } = props; const { name, Icon, categories, tags } = entry; const weight = useRecoilValue(iconWeightAtom); @@ -78,10 +79,10 @@ const DetailsPanel: React.FC = (props) => { [name] ); - const buttonBarStyle: React.CSSProperties = { + const buttonBarStyle: CSSProperties = { color: isDark ? "white" : buttonColor, }; - const snippetButtonStyle: React.CSSProperties = + const snippetButtonStyle: CSSProperties = weight === "duotone" ? { color: disabledColor, userSelect: "none" } : { color: buttonColor }; diff --git a/src/components/IconGrid/IconGrid.tsx b/src/components/IconGrid/IconGrid.tsx index 806dcadf..9b9f4206 100644 --- a/src/components/IconGrid/IconGrid.tsx +++ b/src/components/IconGrid/IconGrid.tsx @@ -1,17 +1,18 @@ -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 { iconWeightAtom, iconSizeAtom, iconColorAtom } from "../../state/atoms"; +import { iconWeightAtom, iconSizeAtom, iconColorAtom } from "@/state/atoms"; import { filteredQueryResultsSelector, isDarkThemeSelector, -} from "../../state/selectors"; -import useGridSpans from "../../hooks/useGridSpans"; +} from "@/state/selectors"; +import useGridSpans from "@/hooks/useGridSpans"; +import Notice from "@/components/Notice"; + import IconGridItem from "./IconGridItem"; import TagCloud from "./TagCloud"; -import Notice from "../Notice/Notice"; import "./IconGrid.css"; const defaultSearchTags = [ @@ -26,7 +27,7 @@ const defaultSearchTags = [ type IconGridProps = {}; -const IconGrid: React.FC = () => { +const IconGrid = (_: IconGridProps) => { const weight = useRecoilValue(iconWeightAtom); const size = useRecoilValue(iconSizeAtom); const color = useRecoilValue(iconColorAtom); diff --git a/src/components/IconGrid/IconGridItem.tsx b/src/components/IconGrid/IconGridItem.tsx index 9ef19f34..c82a3676 100644 --- a/src/components/IconGrid/IconGridItem.tsx +++ b/src/components/IconGrid/IconGridItem.tsx @@ -1,15 +1,11 @@ -import React, { - useRef, - useLayoutEffect, - useEffect, - MutableRefObject, -} from "react"; +import { useRef, useLayoutEffect, useEffect, MutableRefObject } from "react"; import { useRecoilState } from "recoil"; import { motion, AnimatePresence } from "framer-motion"; -import { iconPreviewOpenAtom } from "../../state/atoms"; +import { IconEntry } from "@/lib"; +import { iconPreviewOpenAtom } from "@/state/atoms"; + import DetailsPanel from "./DetailsPanel"; -import { IconEntry } from "../../lib"; interface IconGridItemProps { index: number; @@ -31,7 +27,7 @@ const itemVariants = { }), }; -const IconGridItem: React.FC = (props) => { +const IconGridItem = (props: IconGridItemProps) => { const { index, originOffset, entry } = props; const { name, Icon } = entry; const [open, setOpen] = useRecoilState(iconPreviewOpenAtom); diff --git a/src/components/IconGrid/TagCloud.tsx b/src/components/IconGrid/TagCloud.tsx index 85121f8b..e2ab621e 100644 --- a/src/components/IconGrid/TagCloud.tsx +++ b/src/components/IconGrid/TagCloud.tsx @@ -1,7 +1,7 @@ -import React, { useCallback } from "react"; +import { useCallback } from "react"; import { useSetRecoilState } from "recoil"; -import { searchQueryAtom } from "../../state/atoms"; +import { searchQueryAtom } from "@/state/atoms"; import "./TagCloud.css"; interface TagCloudProps { @@ -10,7 +10,7 @@ interface TagCloudProps { isDark: boolean; } -const TagCloud: React.FC = ({ name, tags, isDark }) => { +const TagCloud = ({ name, tags, isDark }: TagCloudProps) => { const setQuery = useSetRecoilState(searchQueryAtom); const handleTagClick = useCallback( (tag: string) => { diff --git a/src/components/IconGrid/index.ts b/src/components/IconGrid/index.ts new file mode 100644 index 00000000..4078c88d --- /dev/null +++ b/src/components/IconGrid/index.ts @@ -0,0 +1 @@ +export { default } from "./IconGrid"; diff --git a/src/components/Links/Links.tsx b/src/components/Links/Links.tsx index 8c8b41ef..c4d458be 100644 --- a/src/components/Links/Links.tsx +++ b/src/components/Links/Links.tsx @@ -1,14 +1,13 @@ -import React from "react"; import { OutboundLink } from "react-ga"; import { ArrowElbowDownRight } from "phosphor-react"; -import { iconCount } from "../../lib/icons"; +import { iconCount } from "@/lib/icons"; import "./Links.css"; interface LinksProps {} -const Links: React.FC = () => { +const Links = (_: LinksProps) => { return (
diff --git a/src/components/Links/index.ts b/src/components/Links/index.ts new file mode 100644 index 00000000..4ca5192b --- /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 d54078b6..43f5082a 100644 --- a/src/components/Notice/Notice.tsx +++ b/src/components/Notice/Notice.tsx @@ -1,21 +1,18 @@ -import React from "react"; +import { ReactNode } from "react"; import { motion } from "framer-motion"; import { useRecoilValue } from "recoil"; - -import { isDarkThemeSelector } from "../../state/selectors"; -import { searchQueryAtom } from "../../state/atoms"; import { HourglassMedium, Question, SmileyXEyes } from "phosphor-react"; +import { isDarkThemeSelector } from "@/state/selectors"; +import { searchQueryAtom } from "@/state/atoms"; + interface NoticeProps { message?: string; type?: "wait" | "help" | "warn" | "none"; + children?: ReactNode; } -const Notice: React.FC = ({ - message, - type = "warn", - children, -}) => { +const Notice = ({ message, type = "warn", children }: NoticeProps) => { const isDark = useRecoilValue(isDarkThemeSelector); const query = useRecoilValue(searchQueryAtom); diff --git a/src/components/Notice/index.ts b/src/components/Notice/index.ts new file mode 100644 index 00000000..d9ef1386 --- /dev/null +++ b/src/components/Notice/index.ts @@ -0,0 +1 @@ +export { default } from "./Notice"; diff --git a/src/components/SearchInput/SearchInput.tsx b/src/components/SearchInput/SearchInput.tsx index 25ec8d67..e8864479 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 { searchQueryAtom } from "../../state/atoms"; +import { searchQueryAtom } from "@/state/atoms"; 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 = @@ -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 00000000..1a2fa40b --- /dev/null +++ b/src/components/SearchInput/index.ts @@ -0,0 +1 @@ +export { default } from "./SearchInput"; diff --git a/src/components/SettingsActions/SettingsActions.tsx b/src/components/SettingsActions/SettingsActions.tsx index f7ef9810..ea9b34e1 100644 --- a/src/components/SettingsActions/SettingsActions.tsx +++ b/src/components/SettingsActions/SettingsActions.tsx @@ -1,12 +1,13 @@ -import React from "react"; -import { ArrowCounterClockwise, CheckCircle, Link } from "phosphor-react"; import { useRecoilValue, useResetRecoilState } from "recoil"; -import { iconWeightAtom, iconSizeAtom, iconColorAtom } from "../../state/atoms"; +import { ArrowCounterClockwise, CheckCircle, Link } from "phosphor-react"; + +import { iconWeightAtom, iconSizeAtom, iconColorAtom } from "@/state/atoms"; +import useTransientState from "@/hooks/useTransientState"; +import { resetSettingsSelector } from "@/state/selectors"; + import "./SettingsActions.css"; -import useTransientState from "../../hooks/useTransientState"; -import { resetSettingsSelector } from "../../state/selectors"; -const SettingsActions: React.FC = () => { +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 00000000..1e0e12ef --- /dev/null +++ b/src/components/SettingsActions/index.ts @@ -0,0 +1 @@ +export { default } from "./SettingsActions"; diff --git a/src/components/SizeInput/SizeInput.tsx b/src/components/SizeInput/SizeInput.tsx index d8e37be4..071077f0 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/atoms"; 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 00000000..eb5f6d1f --- /dev/null +++ b/src/components/SizeInput/index.ts @@ -0,0 +1 @@ +export { default } from "./SizeInput"; diff --git a/src/components/StyleInput/StyleInput.tsx b/src/components/StyleInput/StyleInput.tsx index cd52a5c1..f2843ffa 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 { IconStyle } from "@phosphor-icons/core"; -import { iconWeightAtom } from "../../state/atoms"; +import { iconWeightAtom } from "@/state/atoms"; + 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 00000000..4f7b1861 --- /dev/null +++ b/src/components/StyleInput/index.ts @@ -0,0 +1 @@ +export { default } from "./StyleInput"; diff --git a/src/components/Toolbar/Toolbar.tsx b/src/components/Toolbar/Toolbar.tsx index ac3c17f9..417b33cb 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 00000000..cec4f38c --- /dev/null +++ b/src/components/Toolbar/index.ts @@ -0,0 +1 @@ +export { default } from "./Toolbar"; diff --git a/src/index.tsx b/src/index.tsx index 9a0e59dd..dbab96d9 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 App from "./components/App"; import ReactGA from "react-ga"; ReactGA.initialize("UA-179205759-1", { titleCase: false }); ReactGA.pageview(window.location.pathname); -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 92639b35..ec1d7581 100644 --- a/src/lib/icons.ts +++ b/src/lib/icons.ts @@ -4,9 +4,7 @@ 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, })); diff --git a/src/lib/index.ts b/src/lib/index.ts index fece71e6..86e3b588 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 { IconEntry as CoreEntry } from "@phosphor-icons/core"; -export interface IconEntry { - name: string; - categories: IconCategory[]; - tags: string[]; +export interface IconEntry extends CoreEntry { Icon: Icon; } diff --git a/src/react-app-env.d.ts b/src/react-app-env.d.ts deleted file mode 100644 index 6431bc5f..00000000 --- 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 e7b8199e..00000000 --- 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 5fdf0016..00000000 --- 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/selectors.ts b/src/state/selectors.ts index 157aa2aa..7f43e8bc 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"], diff --git a/src/utils/index.ts b/src/utils/index.ts index 60b3979e..87d14970 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, diff --git a/src/vite-env.d.ts b/src/vite-env.d.ts new file mode 100644 index 00000000..d8161248 --- /dev/null +++ b/src/vite-env.d.ts @@ -0,0 +1 @@ +/// diff --git a/tsconfig.json b/tsconfig.json index cf0f8ac3..4cf3ce03 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 00000000..f224d2be --- /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"), + }, + }, +}); From 5e7f85ffdc0d936f48e48638348c89a882e326ce Mon Sep 17 00:00:00 2001 From: rektdeckard Date: Fri, 3 Feb 2023 10:31:15 -0700 Subject: [PATCH 2/8] feat(app): tabbed sticky details panel --- src/components/App/App.css | 26 +++ src/components/IconGrid/DetailFooter.tsx | 247 +++++++++++++++++++++++ src/components/IconGrid/DetailsPanel.tsx | 3 +- src/components/IconGrid/IconGrid.css | 32 ++- src/components/IconGrid/IconGrid.tsx | 2 + src/components/IconGrid/IconGridItem.tsx | 12 +- src/components/IconGrid/TagCloud.css | 2 +- src/components/Tabs/Tabs.css | 32 +++ src/components/Tabs/Tabs.tsx | 34 ++++ src/components/Tabs/index.ts | 2 + src/state/atoms.ts | 16 +- src/state/selectors.ts | 8 +- 12 files changed, 398 insertions(+), 18 deletions(-) create mode 100644 src/components/IconGrid/DetailFooter.tsx create mode 100644 src/components/Tabs/Tabs.css create mode 100644 src/components/Tabs/Tabs.tsx create mode 100644 src/components/Tabs/index.ts diff --git a/src/components/App/App.css b/src/components/App/App.css index 52f74e3d..9ede2c80 100644 --- a/src/components/App/App.css +++ b/src/components/App/App.css @@ -95,6 +95,17 @@ button.main-button svg { /* gap: 24px; */ } +figure { + margin: 0; + display: grid; + place-items: center; +} + +figcaption { + font-size: 14px; + text-align: center; +} + a.main-link { text-decoration: none; position: relative; @@ -128,3 +139,18 @@ a.main-link:hover:after { font-size: 24px; line-height: 0.5em; } + +.card { + border-radius: 8px; + border: 2px solid rgba(163, 159, 171, 0.1); +} + +.card.dark { + color: white; + background-color: #413c48; +} + +.card.light { + color: rgb(53, 49, 61); + background-color: #f6f5f6; +} diff --git a/src/components/IconGrid/DetailFooter.tsx b/src/components/IconGrid/DetailFooter.tsx new file mode 100644 index 00000000..06c4334a --- /dev/null +++ b/src/components/IconGrid/DetailFooter.tsx @@ -0,0 +1,247 @@ +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, X, CheckCircle, Download } from "phosphor-react"; +import ReactGA from "react-ga"; + +import { + iconWeightAtom, + iconSizeAtom, + iconColorAtom, + selectionEntryAtom, +} from "@/state/atoms"; +import { isDarkThemeSelector } from "@/state/selectors"; +import Tabs, { Tab } from "@/components/Tabs"; +import useTransientState from "@/hooks/useTransientState"; +import { IconEntry, SnippetType } from "@/lib"; +import { getCodeSnippets, supportsWeight } from "@/utils"; + +import TagCloud from "./TagCloud"; + +const variants: Variants = { + initial: { opacity: 0 }, + animate: { opacity: 1 }, + exit: { opacity: 0 }, +}; + +const RENDERED_SNIPPETS = [ + SnippetType.REACT, + SnippetType.VUE, + SnippetType.HTML, + SnippetType.FLUTTER, +]; + +const buttonColor = "#35313D"; +const successColor = "#1FA647"; +const disabledColor = "#B7B7B7"; + +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 [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: buttonColor }; + + const tabs = [ + { + header: "Tags", + content: ( + ([ + ...entry.categories, + ...entry.name.split("-"), + ...entry.tags, + ]) + )} + isDark={isDark} + /> + ), + }, + ].concat( + RENDERED_SNIPPETS.map((type) => { + const isWeightSupported = supportsWeight({ type, weight }); + + return { + header: type, + content: ( +
+
+                
+                  {isWeightSupported
+                    ? snippets[type]
+                    : "This weight is not yet supported"}
+                
+                
+              
+
+ ), + }; + }) + ); + + return [snippets, tabs]; + }, [entry, weight, 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; + + setCopied("SVG"); + ref.current && void navigator.clipboard?.writeText(ref.current.outerHTML); + }; + + const handleDownloadSVG = ( + event: React.MouseEvent + ) => { + event.currentTarget.blur(); + if (!entry) return; + if (!ref.current?.outerHTML) return; + + const blob = new Blob([ref.current.outerHTML]); + saveAs( + blob, + `${entry?.name}${weight === "regular" ? "" : `-${weight}`}.svg` + ); + }; + + const handleDownloadPNG = async ( + event: React.MouseEvent + ) => { + event.currentTarget.blur(); + if (!entry) return; + if (!ref.current?.outerHTML) return; + + Svg2Png.save( + ref.current, + `${entry?.name}${weight === "regular" ? "" : `-${weight}`}.png`, + { scaleX: 2.667, scaleY: 2.667 } + ); + }; + + return ( + + {!!entry && ( + +
+
+ +
{entry.name}
+
+ + in ≥ {entry.published_in.toFixed(1)}.0 + +
+ +
+ + + +
+
+ )} +
+ ); +}; + +export default DetailFooter; diff --git a/src/components/IconGrid/DetailsPanel.tsx b/src/components/IconGrid/DetailsPanel.tsx index 642e7deb..1be8e03c 100644 --- a/src/components/IconGrid/DetailsPanel.tsx +++ b/src/components/IconGrid/DetailsPanel.tsx @@ -153,7 +153,8 @@ const DetailsPanel = (props: InfoPanelProps) => { className="icon-preview" > -

{name}

+

{name}

+

in ≥ {entry.published_in.toFixed(1)}.0

p.name { + font-size: 16px; +} + +.versioning { + margin-top: 2px; + opacity: 0.6; +} + .icon-usage { flex: 1; padding: 56px 10% 56px 0; @@ -201,3 +211,23 @@ position: relative; top: -96px; } + +aside.detail-footer { + position: sticky; + bottom: 16px; + margin: auto; + max-width: 1120px; + display: grid; + grid-template-columns: 144px 1fr 160px; +} + +.detail-preview { + display: flex; + flex-direction: column; + align-items: center; + padding: 16px; +} + +.detail-actions { + padding: 16px; +} diff --git a/src/components/IconGrid/IconGrid.tsx b/src/components/IconGrid/IconGrid.tsx index 9b9f4206..b2f06af6 100644 --- a/src/components/IconGrid/IconGrid.tsx +++ b/src/components/IconGrid/IconGrid.tsx @@ -11,6 +11,7 @@ import { import useGridSpans from "@/hooks/useGridSpans"; import Notice from "@/components/Notice"; +import DetailFooter from "./DetailFooter"; import IconGridItem from "./IconGridItem"; import TagCloud from "./TagCloud"; import "./IconGrid.css"; @@ -75,6 +76,7 @@ const IconGrid = (_: IconGridProps) => { /> ))} +
); diff --git a/src/components/IconGrid/IconGridItem.tsx b/src/components/IconGrid/IconGridItem.tsx index c82a3676..df22720e 100644 --- a/src/components/IconGrid/IconGridItem.tsx +++ b/src/components/IconGrid/IconGridItem.tsx @@ -3,7 +3,7 @@ import { useRecoilState } from "recoil"; import { motion, AnimatePresence } from "framer-motion"; import { IconEntry } from "@/lib"; -import { iconPreviewOpenAtom } from "@/state/atoms"; +import { iconPreviewOpenAtom, selectionEntryAtom } from "@/state/atoms"; import DetailsPanel from "./DetailsPanel"; @@ -30,15 +30,15 @@ const itemVariants = { const IconGridItem = (props: IconGridItemProps) => { const { index, originOffset, entry } = 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 @@ -88,9 +88,9 @@ const IconGridItem = (props: IconGridItemProps) => { {isUpdated && }

- + {/* {isOpen && } - + */} ); }; diff --git a/src/components/IconGrid/TagCloud.css b/src/components/IconGrid/TagCloud.css index 793edd6b..e5975089 100644 --- a/src/components/IconGrid/TagCloud.css +++ b/src/components/IconGrid/TagCloud.css @@ -2,7 +2,7 @@ display: flex; flex-wrap: wrap; justify-content: center; - padding: 24px; + /* padding: 24px; */ } button.tag-button { diff --git a/src/components/Tabs/Tabs.css b/src/components/Tabs/Tabs.css new file mode 100644 index 00000000..36428ae8 --- /dev/null +++ b/src/components/Tabs/Tabs.css @@ -0,0 +1,32 @@ +.tabs { + display: flex; + flex-direction: column; + border-left: 2px solid rgba(163, 159, 171, 0.1); + border-right: 2px solid rgba(163, 159, 171, 0.1); +} + +.tabs-header { + display: flex; + align-items: center; + gap: 8px; + border-bottom: 2px solid rgba(163, 159, 171, 0.1); +} + +button.tab { + all: unset; + padding: 4px; + font-size: 12px; + text-align: center; + cursor: pointer; + flex: 1; +} + +.tab.active { + background-color: rgba(194, 186, 196, 0.25); +} + +.tab-content { + flex: 1; + display: grid; + place-items: center; +} diff --git a/src/components/Tabs/Tabs.tsx b/src/components/Tabs/Tabs.tsx new file mode 100644 index 00000000..06d1ecd3 --- /dev/null +++ b/src/components/Tabs/Tabs.tsx @@ -0,0 +1,34 @@ +import { ReactNode, useState } from "react"; + +import "./Tabs.css"; + +export type Tab = { + header: ReactNode; + content: ReactNode; +}; + +type TabsProps = { + tabs: Tab[]; +}; + +const Tabs = ({ tabs }: TabsProps) => { + const [activeIndex, setActiveIndex] = useState(0); + + return ( +
+
+ {tabs.map((tab, i) => ( + + ))} +
+
{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 00000000..18ff5c39 --- /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/state/atoms.ts b/src/state/atoms.ts index 2afb5c80..b6d570fa 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/selectors.ts b/src/state/selectors.ts index 7f43e8bc..d76514a7 100644 --- a/src/state/selectors.ts +++ b/src/state/selectors.ts @@ -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(), }); From 37563741404e27bfc7e76e9b18056e271251155f Mon Sep 17 00:00:00 2001 From: rektdeckard Date: Sun, 5 Feb 2023 17:57:52 -0700 Subject: [PATCH 3/8] feat(analytics): migrate to GA4 --- package.json | 3 +- src/components/Banner/Banner.tsx | 2 +- src/components/IconGrid/DetailFooter.tsx | 6 +- src/components/IconGrid/DetailsPanel.tsx | 2 +- src/components/Links/Links.css | 1 + src/components/Links/Links.tsx | 53 +++++++--------- src/components/OutboundLink/OutboundLink.tsx | 64 ++++++++++++++++++++ src/components/OutboundLink/index.ts | 1 + src/components/SearchInput/SearchInput.tsx | 2 +- src/components/Tabs/Tabs.tsx | 1 + src/index.tsx | 6 +- 11 files changed, 99 insertions(+), 42 deletions(-) create mode 100644 src/components/OutboundLink/OutboundLink.tsx create mode 100644 src/components/OutboundLink/index.ts diff --git a/package.json b/package.json index 1fecf57e..4c60ff09 100644 --- a/package.json +++ b/package.json @@ -36,7 +36,7 @@ "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-use": "^17.4.0", "recoil": "^0.7.6", @@ -49,7 +49,6 @@ "@types/node": "^18.11.18", "@types/react": "^18.0.27", "@types/react-dom": "^18.0.10", - "@types/react-virtualized": "^9.21.10", "@types/tinycolor2": "^1.4.3", "@vitejs/plugin-react": "^3.1.0", "typescript": "^4.9.5", diff --git a/src/components/Banner/Banner.tsx b/src/components/Banner/Banner.tsx index 66e2628d..8d5c6eba 100644 --- a/src/components/Banner/Banner.tsx +++ b/src/components/Banner/Banner.tsx @@ -1,5 +1,5 @@ import { Medal } from "phosphor-react"; -import ReactGA from "react-ga"; +import ReactGA from "react-ga4"; import "./Banner.css"; diff --git a/src/components/IconGrid/DetailFooter.tsx b/src/components/IconGrid/DetailFooter.tsx index 06c4334a..33b2fc7d 100644 --- a/src/components/IconGrid/DetailFooter.tsx +++ b/src/components/IconGrid/DetailFooter.tsx @@ -4,8 +4,8 @@ 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, X, CheckCircle, Download } from "phosphor-react"; -import ReactGA from "react-ga"; +import { Copy, CheckCircle, Download } from "phosphor-react"; +import ReactGA from "react-ga4"; import { iconWeightAtom, @@ -16,7 +16,7 @@ import { import { isDarkThemeSelector } from "@/state/selectors"; import Tabs, { Tab } from "@/components/Tabs"; import useTransientState from "@/hooks/useTransientState"; -import { IconEntry, SnippetType } from "@/lib"; +import { SnippetType } from "@/lib"; import { getCodeSnippets, supportsWeight } from "@/utils"; import TagCloud from "./TagCloud"; diff --git a/src/components/IconGrid/DetailsPanel.tsx b/src/components/IconGrid/DetailsPanel.tsx index 1be8e03c..f02a0564 100644 --- a/src/components/IconGrid/DetailsPanel.tsx +++ b/src/components/IconGrid/DetailsPanel.tsx @@ -5,7 +5,7 @@ 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 ReactGA from "react-ga4"; import { iconWeightAtom, diff --git a/src/components/Links/Links.css b/src/components/Links/Links.css index fd241ef5..710549d1 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 c4d458be..d43e2bc8 100644 --- a/src/components/Links/Links.tsx +++ b/src/components/Links/Links.tsx @@ -1,7 +1,7 @@ -import { OutboundLink } from "react-ga"; import { ArrowElbowDownRight } from "phosphor-react"; import { iconCount } from "@/lib/icons"; +import OutboundLink from "@/components/OutboundLink"; import "./Links.css"; @@ -14,7 +14,7 @@ const Links = (_: LinksProps) => { { Download all ({iconCount})
+
Figma library {" / "} plugin
+
{ Sketch plugin
+ - {/* - - */} + ); diff --git a/src/components/OutboundLink/OutboundLink.tsx b/src/components/OutboundLink/OutboundLink.tsx new file mode 100644 index 00000000..4234e67e --- /dev/null +++ b/src/components/OutboundLink/OutboundLink.tsx @@ -0,0 +1,64 @@ +import { + DetailedHTMLProps, + AnchorHTMLAttributes, + useCallback, + MouseEventHandler, +} from "react"; +import ReactGA from "react-ga4"; +import { UaEventOptions } from "react-ga4/types/ga4"; + +interface OutboundLinkProps + extends DetailedHTMLProps< + AnchorHTMLAttributes, + 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 00000000..b54bc9ac --- /dev/null +++ b/src/components/OutboundLink/index.ts @@ -0,0 +1 @@ +export { default } from "./OutboundLink"; diff --git a/src/components/SearchInput/SearchInput.tsx b/src/components/SearchInput/SearchInput.tsx index e8864479..688f6e37 100644 --- a/src/components/SearchInput/SearchInput.tsx +++ b/src/components/SearchInput/SearchInput.tsx @@ -9,7 +9,7 @@ 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 ReactGA from "react-ga4"; import { searchQueryAtom } from "@/state/atoms"; import "./SearchInput.css"; diff --git a/src/components/Tabs/Tabs.tsx b/src/components/Tabs/Tabs.tsx index 06d1ecd3..5de51a20 100644 --- a/src/components/Tabs/Tabs.tsx +++ b/src/components/Tabs/Tabs.tsx @@ -19,6 +19,7 @@ const Tabs = ({ tabs }: TabsProps) => {
{tabs.map((tab, i) => ( + + +
+ -
- - - -
)} diff --git a/src/components/IconGrid/DetailsPanel.tsx b/src/components/IconGrid/DetailsPanel.tsx index f02a0564..8c6609eb 100644 --- a/src/components/IconGrid/DetailsPanel.tsx +++ b/src/components/IconGrid/DetailsPanel.tsx @@ -7,14 +7,14 @@ import { saveAs } from "file-saver"; import { Copy, X, CheckCircle, Download } from "phosphor-react"; import ReactGA from "react-ga4"; +import { useTransientState } from "@/hooks"; +import { IconEntry, SnippetType } from "@/lib"; import { iconWeightAtom, iconSizeAtom, iconColorAtom, iconPreviewOpenAtom, -} from "@/state/atoms"; -import useTransientState from "@/hooks/useTransientState"; -import { IconEntry, SnippetType } from "@/lib"; +} from "@/state"; import { getCodeSnippets, supportsWeight } from "@/utils"; import TagCloud from "./TagCloud"; diff --git a/src/components/IconGrid/IconGrid.css b/src/components/IconGrid/IconGrid.css index 2a58caac..6a25fe85 100644 --- a/src/components/IconGrid/IconGrid.css +++ b/src/components/IconGrid/IconGrid.css @@ -119,14 +119,15 @@ } .snippet { - margin-bottom: 24px; + /* margin-bottom: 24px; */ + width: 100%; } .snippet pre { display: flex; align-items: center; text-overflow: ellipsis; - color: black; + /* color: black; */ -moz-user-select: all; -webkit-user-select: all; user-select: all; @@ -218,16 +219,40 @@ aside.detail-footer { margin: auto; max-width: 1120px; display: grid; - grid-template-columns: 144px 1fr 160px; + 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; - align-items: center; - padding: 16px; + justify-content: center; + gap: 24px; } .detail-actions { - padding: 16px; + display: flex; + flex-direction: row; + align-items: center; + gap: 8px; } diff --git a/src/components/IconGrid/IconGrid.tsx b/src/components/IconGrid/IconGrid.tsx index b2f06af6..4679ae91 100644 --- a/src/components/IconGrid/IconGrid.tsx +++ b/src/components/IconGrid/IconGrid.tsx @@ -1,13 +1,15 @@ -import { useRef, useEffect } from "react"; +import { useRef, useEffect, CSSProperties } from "react"; import { useRecoilValue } from "recoil"; import { motion, useAnimation } from "framer-motion"; import { IconContext } from "phosphor-react"; -import { iconWeightAtom, iconSizeAtom, iconColorAtom } from "@/state/atoms"; import { + iconWeightAtom, + iconSizeAtom, + iconColorAtom, filteredQueryResultsSelector, isDarkThemeSelector, -} from "@/state/selectors"; +} from "@/state"; import useGridSpans from "@/hooks/useGridSpans"; import Notice from "@/components/Notice"; @@ -26,6 +28,13 @@ const defaultSearchTags = [ "weather", ]; +const gridStyle: Record = { + light: {}, + dark: { + backgroundColor: "#35313D", + }, +} as const; + type IconGridProps = {}; const IconGrid = (_: IconGridProps) => { @@ -56,15 +65,10 @@ const IconGrid = (_: IconGridProps) => {
- + {filteredQueryResults.map((iconEntry, index) => ( { className="grid-item" key={name} ref={ref} - tabIndex={0} + tabIndex={1} style={{ order: index, backgroundColor: isOpen ? "rgba(163, 159, 171, 0.1)" : undefined, @@ -88,9 +86,6 @@ const IconGridItem = (props: IconGridItemProps) => { {isUpdated && }

- {/* - {isOpen && } - */} ); }; diff --git a/src/components/IconGrid/TagCloud.tsx b/src/components/IconGrid/TagCloud.tsx index e2ab621e..817163ba 100644 --- a/src/components/IconGrid/TagCloud.tsx +++ b/src/components/IconGrid/TagCloud.tsx @@ -1,7 +1,7 @@ import { useCallback } from "react"; import { useSetRecoilState } from "recoil"; -import { searchQueryAtom } from "@/state/atoms"; +import { searchQueryAtom } from "@/state"; import "./TagCloud.css"; interface TagCloudProps { diff --git a/src/components/Notice/Notice.tsx b/src/components/Notice/Notice.tsx index 43f5082a..cacc302f 100644 --- a/src/components/Notice/Notice.tsx +++ b/src/components/Notice/Notice.tsx @@ -3,8 +3,7 @@ import { motion } from "framer-motion"; import { useRecoilValue } from "recoil"; import { HourglassMedium, Question, SmileyXEyes } from "phosphor-react"; -import { isDarkThemeSelector } from "@/state/selectors"; -import { searchQueryAtom } from "@/state/atoms"; +import { searchQueryAtom, isDarkThemeSelector } from "@/state"; interface NoticeProps { message?: string; diff --git a/src/components/SearchInput/SearchInput.tsx b/src/components/SearchInput/SearchInput.tsx index 688f6e37..de10ec7e 100644 --- a/src/components/SearchInput/SearchInput.tsx +++ b/src/components/SearchInput/SearchInput.tsx @@ -11,7 +11,7 @@ import { useHotkeys } from "react-hotkeys-hook"; import { Command, MagnifyingGlass, X, HourglassHigh } from "phosphor-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; diff --git a/src/components/SettingsActions/SettingsActions.tsx b/src/components/SettingsActions/SettingsActions.tsx index ea9b34e1..9de39afb 100644 --- a/src/components/SettingsActions/SettingsActions.tsx +++ b/src/components/SettingsActions/SettingsActions.tsx @@ -1,9 +1,13 @@ import { useRecoilValue, useResetRecoilState } from "recoil"; import { ArrowCounterClockwise, CheckCircle, Link } from "phosphor-react"; -import { iconWeightAtom, iconSizeAtom, iconColorAtom } from "@/state/atoms"; -import useTransientState from "@/hooks/useTransientState"; -import { resetSettingsSelector } from "@/state/selectors"; +import { useTransientState } from "@/hooks"; +import { + iconWeightAtom, + iconSizeAtom, + iconColorAtom, + resetSettingsSelector, +} from "@/state"; import "./SettingsActions.css"; diff --git a/src/components/SizeInput/SizeInput.tsx b/src/components/SizeInput/SizeInput.tsx index 071077f0..7d8aaf9b 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 = {}; diff --git a/src/components/StyleInput/StyleInput.tsx b/src/components/StyleInput/StyleInput.tsx index f2843ffa..00280944 100644 --- a/src/components/StyleInput/StyleInput.tsx +++ b/src/components/StyleInput/StyleInput.tsx @@ -4,7 +4,7 @@ import Select from "react-dropdown-select"; import { PencilLine } from "phosphor-react"; import { IconStyle } from "@phosphor-icons/core"; -import { iconWeightAtom } from "@/state/atoms"; +import { iconWeightAtom } from "@/state"; import "./StyleInput.css"; diff --git a/src/components/Tabs/Tabs.css b/src/components/Tabs/Tabs.css index 36428ae8..37a7a52d 100644 --- a/src/components/Tabs/Tabs.css +++ b/src/components/Tabs/Tabs.css @@ -1,15 +1,12 @@ .tabs { display: flex; flex-direction: column; - border-left: 2px solid rgba(163, 159, 171, 0.1); - border-right: 2px solid rgba(163, 159, 171, 0.1); } .tabs-header { display: flex; align-items: center; gap: 8px; - border-bottom: 2px solid rgba(163, 159, 171, 0.1); } button.tab { @@ -19,14 +16,25 @@ button.tab { text-align: center; cursor: pointer; flex: 1; + border-top-left-radius: 8px; + border-top-right-radius: 8px; +} + +button.tab:focus-within { + /* background-color: var(--tabs-background); */ } .tab.active { - background-color: rgba(194, 186, 196, 0.25); + background-color: var(--tabs-background); + border-bottom: none; } .tab-content { flex: 1; + padding: 8px 16px; display: grid; place-items: center; + border-radius: 8px; + background-color: var(--tabs-background); + overflow-y: auto; } diff --git a/src/components/Tabs/Tabs.tsx b/src/components/Tabs/Tabs.tsx index 5de51a20..341f6f27 100644 --- a/src/components/Tabs/Tabs.tsx +++ b/src/components/Tabs/Tabs.tsx @@ -1,4 +1,7 @@ -import { ReactNode, useState } from "react"; +import { CSSProperties, ReactNode, useState } from "react"; +import { useRecoilValue } from "recoil"; + +import { isDarkThemeSelector } from "@/state"; import "./Tabs.css"; @@ -11,15 +14,37 @@ type TabsProps = { tabs: Tab[]; }; +type CSSCustomPropertyName = `--${string}`; + +type CSSCustomProperties = { + [property: CSSCustomPropertyName]: string; +}; + +const colorStyles: Record = { + light: { "--tabs-background": "white" }, + dark: { "--tabs-background": "rgba(194, 186, 196, 0.25)" }, +} as const; + +const contentStyles: Record = { + activeLeft: { borderTopLeftRadius: 0 }, + activeRight: { borderTopRightRadius: 0 }, +} as const; + const Tabs = ({ tabs }: TabsProps) => { const [activeIndex, setActiveIndex] = useState(0); + const isDark = useRecoilValue(isDarkThemeSelector); return ( -
+
{tabs.map((tab, i) => ( ))}
-
{tabs[activeIndex]?.content}
+
+ {tabs[activeIndex]?.content} +
); }; diff --git a/src/hooks/index.ts b/src/hooks/index.ts new file mode 100644 index 00000000..17d8bf19 --- /dev/null +++ b/src/hooks/index.ts @@ -0,0 +1,9 @@ +export { default as useDebounce } from "./useDebounce"; +export { default as useEvent } from "./useEvent"; +export { default as useIconParameters } from "./useIconParameters"; +export { default as usePersistSettings } from "./usePersistSettings"; +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/useEvent.ts b/src/hooks/useEvent.ts new file mode 100644 index 00000000..27929a9c --- /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/state/index.ts b/src/state/index.ts new file mode 100644 index 00000000..bd7725ba --- /dev/null +++ b/src/state/index.ts @@ -0,0 +1,2 @@ +export * from "./atoms"; +export * from "./selectors"; From 345acafb4585929aab975c781cb84950df4bcd51 Mon Sep 17 00:00:00 2001 From: rektdeckard Date: Sat, 11 Feb 2023 13:58:33 -0700 Subject: [PATCH 5/8] feat(app): major refactorings and details footer updates --- src/assets/u-arrow-up-left.svg | 8 - src/components/App/App.css | 48 ++-- src/components/App/App.tsx | 41 ++- src/components/Banner/Banner.css | 2 +- src/components/Footer/Footer.css | 19 +- src/components/Footer/Footer.tsx | 49 +++- src/components/Header/Header.css | 2 +- src/components/IconGrid/DetailFooter.tsx | 86 ++++-- src/components/IconGrid/DetailsPanel.tsx | 256 ------------------ src/components/IconGrid/IconGrid.css | 96 +++---- src/components/IconGrid/IconGrid.tsx | 18 +- src/components/IconGrid/IconGridItem.tsx | 8 +- src/components/IconGrid/TagCloud.css | 14 +- src/components/IconGrid/TagCloud.tsx | 8 +- src/components/Notice/Notice.tsx | 5 +- src/components/SearchInput/SearchInput.css | 2 +- src/components/SearchInput/SearchInput.tsx | 4 +- .../SettingsActions/SettingsActions.css | 8 +- src/components/SizeInput/SizeInput.css | 6 +- src/components/StyleInput/StyleInput.css | 39 +-- src/components/Tabs/Tabs.css | 29 +- src/components/Tabs/Tabs.tsx | 34 +-- src/components/Toolbar/Toolbar.css | 4 +- src/hooks/index.ts | 3 + src/hooks/useCSSVariables.ts | 40 +++ src/hooks/useEvent.ts | 2 +- src/hooks/useGridSpans.ts | 18 -- src/hooks/useMediaQuery.ts | 12 + src/hooks/usePersistSettings.ts | 9 +- src/hooks/useSessionState.ts | 35 +++ src/lib/index.ts | 1 + src/state/index.ts | 2 + src/utils/index.ts | 8 + 33 files changed, 376 insertions(+), 540 deletions(-) delete mode 100644 src/assets/u-arrow-up-left.svg delete mode 100644 src/components/IconGrid/DetailsPanel.tsx create mode 100644 src/hooks/useCSSVariables.ts delete mode 100644 src/hooks/useGridSpans.ts create mode 100644 src/hooks/useMediaQuery.ts create mode 100644 src/hooks/useSessionState.ts diff --git a/src/assets/u-arrow-up-left.svg b/src/assets/u-arrow-up-left.svg deleted file mode 100644 index d77add46..00000000 --- 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 2ec16d47..874d6bb9 100644 --- a/src/components/App/App.css +++ b/src/components/App/App.css @@ -1,3 +1,17 @@ +:root { + --red: #ff6e60; + --blue: #397fff; + --yellow: #ffd171; + --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 +38,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; } @@ -77,14 +89,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,11 +121,11 @@ a.main-link:hover:after { } .badge.new { - color: #ff6e60; + color: var(--red); } .badge.updated { - color: #397fff; + color: var(--blue); } .badge { @@ -131,15 +135,15 @@ a.main-link:hover:after { .card { border-radius: 8px; - border: 2px solid rgba(163, 159, 171, 0.1); + border: 2px solid var(--translucent); } -.card.dark { - color: white; - background-color: #413c48; +.primary { + color: var(--foreground); + background-color: var(--background); } -.card.light { - color: rgb(53, 49, 61); - background-color: #f6f5f6; +.secondary { + color: var(--foreground-card); + background-color: var(--background-card); } diff --git a/src/components/App/App.tsx b/src/components/App/App.tsx index a5e3c0dc..6d307624 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 = () => {