feat(app): major refactorings and details footer updates
This commit is contained in:
@@ -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="" />;
|
||||
@@ -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>
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
@@ -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
|
||||
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>
|
||||
|
||||
@@ -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 = {
|
||||
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>
|
||||
|
||||
@@ -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 {
|
||||
|
||||
Reference in New Issue
Block a user