feat(app): major refactorings and details footer updates
This commit is contained in:
@@ -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 |
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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="" />;
|
||||
@@ -17,8 +22,22 @@ 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>
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
padding: 24px;
|
||||
color: white;
|
||||
text-align: center;
|
||||
background-color: #35313d;
|
||||
background-color: var(--eggplant);
|
||||
}
|
||||
|
||||
.banner .main-button {
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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
|
||||
<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" });
|
||||
}}
|
||||
>
|
||||
<UArrowUpLeft />
|
||||
</button>
|
||||
<ArrowULeftUp size="1em" />
|
||||
</motion.button>
|
||||
)}
|
||||
</AnimatePresence>
|
||||
<div className="outro">
|
||||
<Links />
|
||||
<p>
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
header {
|
||||
width: 100%;
|
||||
background-color: #ffd171;
|
||||
background-color: var(--yellow);
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
|
||||
@@ -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 = {
|
||||
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>
|
||||
|
||||
@@ -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 ≥ {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;
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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 }}
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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>}
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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";
|
||||
|
||||
40
src/hooks/useCSSVariables.ts
Normal file
40
src/hooks/useCSSVariables.ts
Normal 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]);
|
||||
}
|
||||
@@ -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
|
||||
);
|
||||
};
|
||||
12
src/hooks/useMediaQuery.ts
Normal file
12
src/hooks/useMediaQuery.ts
Normal 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;
|
||||
}
|
||||
@@ -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]
|
||||
|
||||
35
src/hooks/useSessionState.ts
Normal file
35
src/hooks/useSessionState.ts
Normal 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];
|
||||
}
|
||||
@@ -10,4 +10,5 @@ export enum SnippetType {
|
||||
VUE = "Vue",
|
||||
HTML = "HTML/CSS",
|
||||
FLUTTER = "Flutter",
|
||||
ELM = "Elm",
|
||||
}
|
||||
|
||||
@@ -1,2 +1,4 @@
|
||||
export * from "./atoms";
|
||||
export * from "./selectors";
|
||||
|
||||
export const STORAGE_KEY = "__phosphor_settings__";
|
||||
|
||||
@@ -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 []`,
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user