feat(app): major refactorings and details footer updates

This commit is contained in:
rektdeckard
2023-02-11 13:58:33 -07:00
parent eba876b3ea
commit 345acafb45
33 changed files with 376 additions and 540 deletions

View File

@@ -1,8 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<svg width="58px" height="58px" viewBox="0 0 58 58" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
<title>u-arrow-up-left</title>
<g id="u-arrow-up-left" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd">
<polygon id="bounding-box" transform="translate(29.200000, 29.200000) scale(1, -1) rotate(90.000000) translate(-29.200000, -29.200000) " points="0.4 58 58 58 58 0.4 0.4 0.4"></polygon>
<path d="M38.2,9.4 C39.1941125,9.4 40,10.2058875 40,11.2 C40,12.1444069 39.2726866,12.9189407 38.347628,12.9940331 L38.2,13 L18.4,13 C12.4353125,13 7.6,17.8353125 7.6,23.8 C7.6,29.6684828 12.2805926,34.4437284 18.112283,34.5962419 L18.4,34.6 L44.654,34.6 L36.9272078,26.8727922 C36.2633165,26.2089009 36.2264336,25.1554365 36.8165592,24.4482593 L36.9272078,24.3272078 C37.5910991,23.6633165 38.6445635,23.6264336 39.3517407,24.2165592 L39.4727922,24.3272078 L50.2727922,35.1272078 C50.3086835,35.1630991 50.3430661,35.200499 50.3758368,35.2393043 L50.2727922,35.1272078 C50.322523,35.1769386 50.3687356,35.2288554 50.4114298,35.2826489 C50.4303122,35.3066321 50.4485184,35.3308286 50.4661181,35.3554879 C50.4786457,35.3727743 50.4909825,35.3907223 50.5029573,35.4088392 C50.5204983,35.4356771 50.5373395,35.4626973 50.5534721,35.4901836 C50.5604155,35.501718 50.5673022,35.5137723 50.5740382,35.5258878 C50.5905898,35.5559164 50.606343,35.5862258 50.6212445,35.6170243 C50.6310894,35.6372379 50.6403272,35.6573209 50.6491756,35.6775332 C50.6615763,35.705848 50.6734867,35.7350368 50.6846414,35.7645963 C50.6896953,35.7781611 50.6944267,35.7911991 50.6990025,35.8042794 C50.7106655,35.837281 50.721476,35.8712903 50.7312871,35.9057207 C50.7358977,35.9224356 50.7403085,35.938831 50.7444838,35.9552738 C50.7807852,36.0974825 50.8,36.2464973 50.8,36.4 C50.8,36.6494086 50.7492745,36.8869696 50.6575775,37.1029292 C50.6430903,37.1371689 50.6273615,37.1712925 50.6105085,37.2049973 C50.5965597,37.232636 50.5820362,37.2598508 50.5668455,37.2866377 C50.566621,37.2873207 50.5664341,37.2876503 50.566247,37.2879798 C50.5435288,37.327754 50.5186404,37.3678619 50.492262,37.4068799 C50.4795991,37.4258795 50.4663526,37.4446817 50.4527038,37.463281 C50.4342916,37.488172 50.4153249,37.512692 50.3957513,37.5366974 C50.3915469,37.5419745 50.3875087,37.5468658 50.3834408,37.5517407 L50.3644975,37.5739923 C50.3400259,37.6024091 50.3146818,37.630053 50.2885067,37.6568825 L50.2727922,37.6727922 L39.4727922,48.4727922 C38.7698485,49.1757359 37.6301515,49.1757359 36.9272078,48.4727922 C36.2633165,47.8089009 36.2264336,46.7554365 36.8165592,46.0482593 L36.9272078,45.9272078 L44.654,38.2 L18.4,38.2 C10.4470875,38.2 4,31.7529125 4,23.8 C4,15.951731 10.2785434,9.5699209 18.0868626,9.4033373 L18.4,9.4 L38.2,9.4 Z" fill="#000000" fill-rule="nonzero" transform="translate(27.400000, 29.200000) scale(1, -1) rotate(90.000000) translate(-27.400000, -29.200000) "></path>
</g>
</svg>

Before

Width:  |  Height:  |  Size: 2.9 KiB

View File

@@ -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);
}

View File

@@ -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 = <Notice message="Search error" />;
const waitingFallback = <Notice type="none" message="" />;
@@ -16,9 +21,23 @@ const waitingFallback = <Notice type="none" message="" />;
const App: React.FC<any> = () => {
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 (
<React.StrictMode>
<Fragment>
<Header />
<main>
<Toolbar />
@@ -29,7 +48,7 @@ const App: React.FC<any> = () => {
</ErrorBoundary>
</main>
<Footer />
</React.StrictMode>
</Fragment>
);
};

View File

@@ -4,7 +4,7 @@
padding: 24px;
color: white;
text-align: center;
background-color: #35313d;
background-color: var(--eggplant);
}
.banner .main-button {

View File

@@ -1,5 +1,5 @@
footer {
background-color: #925bff;
background-color: var(--purple);
}
#back-to-top-button {
@@ -8,6 +8,11 @@ footer {
margin: 0;
border-radius: 50%;
z-index: 2;
font-size: 56px;
}
#back-to-top-button svg {
margin-right: unset;
}
.container {
@@ -65,11 +70,7 @@ footer .links {
width: 56px;
height: 56px;
padding: 0;
}
#back-to-top-button svg {
width: 28px;
height: 28px;
font-size: 28px;
}
footer .links {
@@ -133,12 +134,6 @@ footer .links {
top: 276px;
}
/* #command {
position: absolute;
left: 532px;
top: 150px;
} */
.illustrations-footer {
display: initial;
position: absolute;

View File

@@ -1,30 +1,51 @@
import { Coffee, Heart } from "phosphor-react";
import { useRecoilValue } from "recoil";
import { motion, AnimatePresence, Variants } from "framer-motion";
import { Coffee, Heart, ArrowULeftUp } from "phosphor-react";
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 { useMediaQuery } from "@/hooks";
import { selectionEntryAtom } from "@/state";
import "./Footer.css";
type FooterProps = {};
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 (
<footer>
<div className="container">
<button
id="back-to-top-button"
aria-label="back-to-top button"
className="main-button"
onClick={() => {
document
.getElementById("root")
?.scrollIntoView({ behavior: "smooth", block: "start" });
}}
>
<UArrowUpLeft />
</button>
<AnimatePresence initial={false}>
{(!isMobile || !isViewing) && (
<motion.button
id="back-to-top-button"
aria-label="back-to-top button"
className="main-button"
variants={variants}
initial="initial"
animate="animate"
exit="exit"
transition={{ duration: 0.1 }}
onClick={() => {
document
.getElementById("root")
?.scrollIntoView({ behavior: "smooth", block: "start" });
}}
>
<ArrowULeftUp size="1em" />
</motion.button>
)}
</AnimatePresence>
<div className="outro">
<Links />
<p>

View File

@@ -1,6 +1,6 @@
header {
width: 100%;
background-color: #ffd171;
background-color: var(--yellow);
overflow: hidden;
}

View File

@@ -4,11 +4,11 @@ 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 } from "phosphor-react";
import { Copy, CheckCircle, DownloadSimple, XCircle } from "phosphor-react";
import ReactGA from "react-ga4";
import Tabs, { Tab } from "@/components/Tabs";
import { useTransientState } from "@/hooks";
import { useMediaQuery, useTransientState, useSessionState } from "@/hooks";
import { SnippetType } from "@/lib";
import {
iconWeightAtom,
@@ -21,10 +21,17 @@ import { getCodeSnippets, supportsWeight } from "@/utils";
import TagCloud from "./TagCloud";
const variants: Variants = {
initial: { y: 188 },
animate: { y: 0 },
exit: { y: 188 },
const variants: Record<string, Variants> = {
desktop: {
initial: { y: 188 },
animate: { y: 0 },
exit: { y: 188 },
},
mobile: {
initial: { y: "60vh" },
animate: { y: 0 },
exit: { y: "60vh" },
},
};
const RENDERED_SNIPPETS = [
@@ -32,12 +39,20 @@ const RENDERED_SNIPPETS = [
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);
@@ -51,6 +66,10 @@ const DetailFooter = () => {
);
const ref = useRef<SVGSVGElement>(null);
const [{ i }, setInitialTab] = useSessionState("tab", { i: 0 });
const isMobile = useMediaQuery("(max-width: 719px)");
const [snippets, tabs] = useMemo<
[Partial<Record<SnippetType, string>>, Tab[]]
>(() => {
@@ -67,7 +86,7 @@ const DetailFooter = () => {
const snippetButtonStyle: CSSProperties =
weight === "duotone"
? { color: disabledColor, userSelect: "none" }
: { color: buttonColor };
: { color: "currentcolor" };
const tabs = [
{
@@ -104,7 +123,11 @@ const DetailFooter = () => {
title="Copy snippet"
onClick={(e) => handleCopySnippet(e, type)}
disabled={!isWeightSupported}
style={isWeightSupported ? undefined : snippetButtonStyle}
style={
isWeightSupported
? { color: "currentColor" }
: snippetButtonStyle
}
>
{copied === type ? (
<CheckCircle size={24} color={successColor} weight="fill" />
@@ -113,7 +136,7 @@ const DetailFooter = () => {
size={24}
color={
isWeightSupported
? buttonColor
? "currentColor"
: snippetButtonStyle.color
}
weight="fill"
@@ -128,7 +151,7 @@ const DetailFooter = () => {
);
return [snippets, tabs];
}, [entry, weight, copied, isDark]);
}, [entry, weight, size, copied, isDark]);
useHotkeys("esc", () => setSelectionEntry(null));
@@ -163,9 +186,10 @@ const DetailFooter = () => {
) => {
event.currentTarget.blur();
if (!entry) return;
if (!ref.current) return;
navigator.clipboard?.writeText(cloneWithSize(ref.current, size).outerHTML);
setCopied("SVG");
ref.current && void navigator.clipboard?.writeText(ref.current.outerHTML);
};
const handleDownloadSVG = (
@@ -173,9 +197,9 @@ const DetailFooter = () => {
) => {
event.currentTarget.blur();
if (!entry) return;
if (!ref.current?.outerHTML) return;
if (!ref.current) return;
const blob = new Blob([ref.current.outerHTML]);
const blob = new Blob([cloneWithSize(ref.current, size).outerHTML]);
saveAs(
blob,
`${entry?.name}${weight === "regular" ? "" : `-${weight}`}.svg`
@@ -187,12 +211,11 @@ const DetailFooter = () => {
) => {
event.currentTarget.blur();
if (!entry) return;
if (!ref.current?.outerHTML) return;
if (!ref.current) return;
Svg2Png.save(
ref.current,
`${entry?.name}${weight === "regular" ? "" : `-${weight}`}.png`,
{ scaleX: 2.667, scaleY: 2.667 }
cloneWithSize(ref.current, size),
`${entry?.name}${weight === "regular" ? "" : `-${weight}`}.png`
);
};
@@ -203,9 +226,9 @@ const DetailFooter = () => {
initial="initial"
animate="animate"
exit="exit"
variants={variants}
className={`detail-footer card ${isDark ? "dark" : "light"}`}
transition={{ duration: 0.1 }}
variants={isMobile ? variants.mobile : variants.desktop}
className="secondary detail-footer card"
transition={isMobile ? { duration: 0.25 } : { duration: 0.1 }}
>
<div className="detail-preview">
<figure>
@@ -223,14 +246,16 @@ const DetailFooter = () => {
style={buttonBarStyle}
onClick={handleDownloadPNG}
>
<DownloadSimple size={24} color="currentColor" weight="fill" /> PNG
<DownloadSimple size={24} color="currentColor" weight="fill" />{" "}
PNG
</button>
<button
tabIndex={0}
style={buttonBarStyle}
onClick={handleDownloadSVG}
>
<DownloadSimple size={24} color="currentColor" weight="fill" /> SVG
<DownloadSimple size={24} color="currentColor" weight="fill" />{" "}
SVG
</button>
<button
tabIndex={0}
@@ -247,7 +272,22 @@ const DetailFooter = () => {
</div>
</div>
<Tabs tabs={tabs} />
<Tabs
tabs={tabs}
initialIndex={i}
onTabChange={(i) => setInitialTab({ i })}
/>
<button
tabIndex={0}
className="close-button"
onClick={() => setSelectionEntry(null)}
onKeyDown={(e) => {
e.key === "Enter" && setSelectionEntry(null);
}}
>
<XCircle color="currentColor" size={28} weight="fill" />
</button>
</motion.aside>
)}
</AnimatePresence>

View File

@@ -1,256 +0,0 @@
import React, { useRef, useEffect, CSSProperties } 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-ga4";
import { useTransientState } from "@/hooks";
import { IconEntry, SnippetType } from "@/lib";
import {
iconWeightAtom,
iconSizeAtom,
iconColorAtom,
iconPreviewOpenAtom,
} from "@/state";
import { getCodeSnippets, supportsWeight } from "@/utils";
import TagCloud from "./TagCloud";
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 = (props: InfoPanelProps) => {
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<SnippetType | "SVG" | false>(
false,
2000
);
const ref = useRef<SVGSVGElement>(null);
useHotkeys("esc", () => setOpen(false));
useEffect(
() => ReactGA.event({ category: "Grid", action: "Details", label: name }),
[name]
);
const buttonBarStyle: CSSProperties = {
color: isDark ? "white" : buttonColor,
};
const snippetButtonStyle: CSSProperties =
weight === "duotone"
? { color: disabledColor, userSelect: "none" }
: { color: buttonColor };
const snippets = getCodeSnippets({
displayName: Icon.displayName!,
name,
weight,
size,
color,
});
const handleCopySnippet = (
event: React.MouseEvent<HTMLButtonElement, MouseEvent>,
type: SnippetType
) => {
event.currentTarget.blur();
setCopied(type);
const data = snippets[type];
data && void navigator.clipboard?.writeText(data);
};
const handleCopySVG = (
event: React.MouseEvent<HTMLButtonElement, MouseEvent>
) => {
event.currentTarget.blur();
setCopied("SVG");
ref.current && void navigator.clipboard?.writeText(ref.current.outerHTML);
};
const handleDownloadSVG = (
event: React.MouseEvent<HTMLButtonElement, 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<HTMLButtonElement, 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 (
<motion.section
className="info-box"
animate="open"
exit="collapsed"
variants={panelVariants}
style={{
order: index + (spans - (index % spans)),
color: isDark ? "white" : "black",
}}
>
<motion.div
initial="collapsed"
animate="open"
exit="collapsed"
variants={contentVariants}
className="icon-preview"
>
<Icon ref={ref} color={color} weight={weight} size={192} />
<p className="name">{name}</p>
<p className="versioning">in &ge; {entry.published_in.toFixed(1)}.0</p>
<TagCloud
name={name}
tags={Array.from(
new Set<string>([...categories, ...name.split("-"), ...tags])
)}
isDark={isDark}
/>
</motion.div>
<motion.div
initial="collapsed"
animate="open"
exit="collapsed"
variants={contentVariants}
className="icon-usage"
>
{renderedSnippets.map((type) => {
const isWeightSupported = supportsWeight({ type, weight });
return (
<div className="snippet" key={type}>
{type}
<pre
tabIndex={0}
style={isWeightSupported ? undefined : snippetButtonStyle}
>
<span>
{isWeightSupported
? snippets[type]
: "This weight is not yet supported"}
</span>
<button
title="Copy snippet"
onClick={(e) => handleCopySnippet(e, type)}
disabled={!isWeightSupported}
style={isWeightSupported ? undefined : snippetButtonStyle}
>
{copied === type ? (
<CheckCircle size={24} color={successColor} weight="fill" />
) : (
<Copy
size={24}
color={
isWeightSupported
? buttonColor
: snippetButtonStyle.color
}
weight="fill"
/>
)}
</button>
</pre>
</div>
);
})}
<div className="button-row">
<button style={buttonBarStyle} onClick={handleDownloadPNG}>
<Download size={32} color="currentColor" weight="fill" /> Download
PNG
</button>
<button style={buttonBarStyle} onClick={handleDownloadSVG}>
<Download size={32} color="currentColor" weight="fill" /> Download
SVG
</button>
<button style={buttonBarStyle} onClick={handleCopySVG}>
{copied === "SVG" ? (
<CheckCircle size={32} color={successColor} weight="fill" />
) : (
<Copy size={32} color="currentColor" weight="fill" />
)}
{copied === "SVG" ? "Copied!" : "Copy SVG"}
</button>
</div>
</motion.div>
<motion.span
initial="collapsed"
animate="open"
exit="collapsed"
variants={contentVariants}
title="Close"
>
<X
className="close-icon"
tabIndex={0}
color={buttonBarStyle.color}
size={32}
weight="fill"
onClick={() => setOpen(false)}
onKeyDown={(e) => {
e.key === "Enter" && setOpen(false);
}}
/>
</motion.span>
</motion.section>
);
};
export default DetailsPanel;

View File

@@ -3,6 +3,8 @@
padding: 32px 16px;
/* min-height: 80vh; */
content-visibility: auto;
color: var(--foreground);
background-color: var(--background);
}
.grid {
@@ -27,22 +29,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;
}
@@ -65,61 +66,12 @@
}
}
.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-preview > p.name {
font-size: 16px;
}
.versioning {
margin-top: 2px;
opacity: 0.6;
}
.icon-usage {
flex: 1;
padding: 56px 10% 56px 0;
}
.snippet {
/* margin-bottom: 24px; */
width: 100%;
}
@@ -127,7 +79,6 @@
display: flex;
align-items: center;
text-overflow: ellipsis;
/* color: black; */
-moz-user-select: all;
-webkit-user-select: all;
user-select: all;
@@ -182,12 +133,30 @@
.close-icon {
position: absolute;
top: 24px;
right: 24px;
top: 12px;
right: 12px;
text-align: end;
cursor: pointer;
}
.close-button {
color: inherit;
background: var(--background);
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:active {
opacity: 0.7;
}
.empty-list {
display: flex;
flex-direction: column;
@@ -256,3 +225,18 @@ figcaption > p {
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: 60vh;
}
}

View File

@@ -1,4 +1,4 @@
import { useRef, useEffect, CSSProperties } from "react";
import { useRef, useEffect } from "react";
import { useRecoilValue } from "recoil";
import { motion, useAnimation } from "framer-motion";
import { IconContext } from "phosphor-react";
@@ -10,7 +10,6 @@ import {
filteredQueryResultsSelector,
isDarkThemeSelector,
} from "@/state";
import useGridSpans from "@/hooks/useGridSpans";
import Notice from "@/components/Notice";
import DetailFooter from "./DetailFooter";
@@ -28,13 +27,6 @@ const defaultSearchTags = [
"weather",
];
const gridStyle: Record<string, CSSProperties> = {
light: {},
dark: {
backgroundColor: "#35313D",
},
} as const;
type IconGridProps = {};
const IconGrid = (_: IconGridProps) => {
@@ -42,8 +34,6 @@ const IconGrid = (_: IconGridProps) => {
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 });
@@ -63,17 +53,13 @@ const IconGrid = (_: IconGridProps) => {
return (
<IconContext.Provider value={{ weight, size, color, mirrored: false }}>
<div
className="grid-container"
style={isDark ? gridStyle.dark : gridStyle.light}
>
<div className="grid-container">
<i id="beacon" className="beacon" />
<motion.div className="grid" initial="hidden" animate={controls}>
{filteredQueryResults.map((iconEntry, index) => (
<IconGridItem
key={index}
index={index}
spans={spans}
isDark={isDark}
entry={iconEntry}
originOffset={originOffset}

View File

@@ -7,7 +7,6 @@ import { selectionEntryAtom } from "@/state";
interface IconGridItemProps {
index: number;
spans: number;
isDark: boolean;
entry: IconEntry;
originOffset: MutableRefObject<{ top: number; left: number }>;
@@ -68,11 +67,8 @@ const IconGridItem = (props: IconGridItemProps) => {
className="grid-item"
key={name}
ref={ref}
tabIndex={1}
style={{
order: index,
backgroundColor: isOpen ? "rgba(163, 159, 171, 0.1)" : undefined,
}}
tabIndex={0}
style={isOpen ? { backgroundColor: "var(--translucent)" } : undefined}
custom={delayRef}
transition={transition}
variants={itemVariants}

View File

@@ -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;
}

View File

@@ -1,6 +1,7 @@
import { useCallback } from "react";
import { useSetRecoilState } from "recoil";
import { useMediaQuery } from "@/hooks";
import { searchQueryAtom } from "@/state";
import "./TagCloud.css";
@@ -11,13 +12,14 @@ interface TagCloudProps {
}
const TagCloud = ({ name, tags, isDark }: 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 +30,7 @@ const TagCloud = ({ name, tags, isDark }: TagCloudProps) => {
className="tag-button"
onClick={() => void handleTagClick(tag)}
>
<code className={`${isDark ? "dark" : ""}`}>{tag}</code>
<code>{tag}</code>
{tag === "*new*" && <span className="badge new"></span>}
{tag === "*updated*" && <span className="badge updated"></span>}
</button>

View File

@@ -3,7 +3,7 @@ import { motion } from "framer-motion";
import { useRecoilValue } from "recoil";
import { HourglassMedium, Question, SmileyXEyes } from "phosphor-react";
import { searchQueryAtom, isDarkThemeSelector } from "@/state";
import { searchQueryAtom } from "@/state";
interface NoticeProps {
message?: string;
@@ -12,11 +12,10 @@ interface NoticeProps {
}
const Notice = ({ message, type = "warn", children }: NoticeProps) => {
const isDark = useRecoilValue(isDarkThemeSelector);
const query = useRecoilValue(searchQueryAtom);
return (
<div style={isDark ? { backgroundColor: "#35313D", color: "white" } : {}}>
<div className="primary">
<motion.div
className="empty-list"
initial={{ opacity: 0 }}

View File

@@ -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 {

View File

@@ -84,8 +84,8 @@ const SearchInput = (_: SearchInputProps) => {
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 && <Keys>{isApple ? <Command /> : "Ctrl + "}K</Keys>}

View File

@@ -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) {

View File

@@ -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);
}

View File

@@ -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 {

View File

@@ -20,21 +20,38 @@ button.tab {
border-top-right-radius: 8px;
}
button.tab:focus-within {
/* background-color: var(--tabs-background); */
button.tab:focus-visible {
outline: 1px solid currentColor;
}
.tab.active {
background-color: var(--tabs-background);
button.tab:hover:not(.active) {
background-color: var(--sheer);
}
button.tab.active {
background-color: var(--background);
border-bottom: none;
}
.tab-content {
flex: 1;
padding: 8px 16px;
height: 80px;
max-height: 80px;
padding: 16px;
display: grid;
place-items: center;
border-radius: 8px;
background-color: var(--tabs-background);
background-color: var(--background);
overflow-y: auto;
}
@media screen and (max-width: 719px) {
.tabs {
flex: 1;
}
.tab-content {
height: unset;
max-height: unset;
}
}

View File

@@ -1,7 +1,4 @@
import { CSSProperties, ReactNode, useState } from "react";
import { useRecoilValue } from "recoil";
import { isDarkThemeSelector } from "@/state";
import "./Tabs.css";
@@ -12,41 +9,32 @@ export type Tab = {
type TabsProps = {
tabs: Tab[];
initialIndex?: number;
onTabChange?: (index: number) => void;
};
type CSSCustomPropertyName = `--${string}`;
type CSSCustomProperties = {
[property: CSSCustomPropertyName]: string;
};
const colorStyles: Record<string, CSSProperties & CSSCustomProperties> = {
light: { "--tabs-background": "white" },
dark: { "--tabs-background": "rgba(194, 186, 196, 0.25)" },
} as const;
const contentStyles: Record<string, CSSProperties> = {
activeLeft: { borderTopLeftRadius: 0 },
activeRight: { borderTopRightRadius: 0 },
} as const;
const Tabs = ({ tabs }: TabsProps) => {
const [activeIndex, setActiveIndex] = useState<number>(0);
const isDark = useRecoilValue(isDarkThemeSelector);
const Tabs = ({ tabs, initialIndex = 0, onTabChange }: TabsProps) => {
const [activeIndex, setActiveIndex] = useState<number>(
!!tabs[initialIndex] ? initialIndex : 0
);
return (
<div
className="tabs"
tabIndex={0}
style={isDark ? colorStyles.dark : colorStyles.light}
>
<div className="secondary tabs" tabIndex={0}>
<div className="tabs-header">
{tabs.map((tab, i) => (
<button
key={i}
tabIndex={0}
className={`tab ${activeIndex === i ? "active" : ""}`}
onClick={() => setActiveIndex(i)}
onClick={() => {
setActiveIndex(i);
onTabChange?.(i);
}}
>
{tab.header}
</button>

View File

@@ -4,12 +4,12 @@ nav.toolbar {
top: -1px;
padding: 0;
margin: 0;
background-color: #35313d;
background-color: var(--eggplant);
z-index: 1;
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 {

View File

@@ -1,7 +1,10 @@
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 useMediaQuery } from "./useMediaQuery";
export { default as usePersistSettings } from "./usePersistSettings";
export { default as useSessionState } from "./useSessionState";
export { default as useThrottle } from "./useThrottle";
export { default as useThrottled } from "./useThrottled";
export { default as useTimeoutFn } from "./useTimeoutFn";

View File

@@ -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<CSSCustomProperties>({});
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]);
}

View File

@@ -18,7 +18,7 @@ export type UseEventType<E extends UseEventTarget> = keyof UseEventMap<E>;
* 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 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

View File

@@ -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
);
};

View File

@@ -0,0 +1,12 @@
import { useMemo, useReducer, Reducer } from "react";
import useEvent from "./useEvent";
const updater: Reducer<number, void> = (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;
}

View File

@@ -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]

View File

@@ -0,0 +1,35 @@
import { useCallback, useState, Dispatch, SetStateAction } from "react";
import { STORAGE_KEY } from "@/state";
type Initializer<S> = () => S;
type Setter<S> = (prev: S) => S;
type Action<S> = S | Setter<S> | Initializer<S>;
function expand<S extends object>(action: Action<S>, prev?: S) {
if (typeof action === "function") {
return (action as Setter<S>)(prev!);
} else {
return action;
}
}
export default function useSessionState<S extends object>(
key: string,
fallbackState: S | (() => S)
): [S, Dispatch<SetStateAction<S>>] {
const [value, setValue] = useState<S>(() => {
let val = sessionStorage.getItem(STORAGE_KEY + key);
if (val) return JSON.parse(val) as S;
return expand(fallbackState);
});
const set: Dispatch<SetStateAction<S>> = useCallback((val) => {
setValue((prev) => {
const next = expand(val, prev);
sessionStorage.setItem(STORAGE_KEY + key, JSON.stringify(next));
return next;
});
}, []);
return [value, set];
}

View File

@@ -10,4 +10,5 @@ export enum SnippetType {
VUE = "Vue",
HTML = "HTML/CSS",
FLUTTER = "Flutter",
ELM = "Elm",
}

View File

@@ -1,2 +1,4 @@
export * from "./atoms";
export * from "./selectors";
export const STORAGE_KEY = "__phosphor_settings__";

View File

@@ -17,6 +17,8 @@ export function getCodeSnippets({
}): Record<SnippetType, string> {
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]: `<i class="ph-${name}${
@@ -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 []`,
};
}