feat(app): tabbed sticky details panel
This commit is contained in:
@@ -95,6 +95,17 @@ button.main-button svg {
|
|||||||
/* gap: 24px; */
|
/* gap: 24px; */
|
||||||
}
|
}
|
||||||
|
|
||||||
|
figure {
|
||||||
|
margin: 0;
|
||||||
|
display: grid;
|
||||||
|
place-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
figcaption {
|
||||||
|
font-size: 14px;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
a.main-link {
|
a.main-link {
|
||||||
text-decoration: none;
|
text-decoration: none;
|
||||||
position: relative;
|
position: relative;
|
||||||
@@ -128,3 +139,18 @@ a.main-link:hover:after {
|
|||||||
font-size: 24px;
|
font-size: 24px;
|
||||||
line-height: 0.5em;
|
line-height: 0.5em;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.card {
|
||||||
|
border-radius: 8px;
|
||||||
|
border: 2px solid rgba(163, 159, 171, 0.1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.card.dark {
|
||||||
|
color: white;
|
||||||
|
background-color: #413c48;
|
||||||
|
}
|
||||||
|
|
||||||
|
.card.light {
|
||||||
|
color: rgb(53, 49, 61);
|
||||||
|
background-color: #f6f5f6;
|
||||||
|
}
|
||||||
|
|||||||
247
src/components/IconGrid/DetailFooter.tsx
Normal file
247
src/components/IconGrid/DetailFooter.tsx
Normal file
@@ -0,0 +1,247 @@
|
|||||||
|
import React, { useRef, useEffect, CSSProperties, useMemo } from "react";
|
||||||
|
import { useRecoilValue, useRecoilState } from "recoil";
|
||||||
|
import { useHotkeys } from "react-hotkeys-hook";
|
||||||
|
import { motion, AnimatePresence, Variants } from "framer-motion";
|
||||||
|
import { Svg2Png } from "svg2png-converter";
|
||||||
|
import { saveAs } from "file-saver";
|
||||||
|
import { Copy, X, CheckCircle, Download } from "phosphor-react";
|
||||||
|
import ReactGA from "react-ga";
|
||||||
|
|
||||||
|
import {
|
||||||
|
iconWeightAtom,
|
||||||
|
iconSizeAtom,
|
||||||
|
iconColorAtom,
|
||||||
|
selectionEntryAtom,
|
||||||
|
} from "@/state/atoms";
|
||||||
|
import { isDarkThemeSelector } from "@/state/selectors";
|
||||||
|
import Tabs, { Tab } from "@/components/Tabs";
|
||||||
|
import useTransientState from "@/hooks/useTransientState";
|
||||||
|
import { IconEntry, SnippetType } from "@/lib";
|
||||||
|
import { getCodeSnippets, supportsWeight } from "@/utils";
|
||||||
|
|
||||||
|
import TagCloud from "./TagCloud";
|
||||||
|
|
||||||
|
const variants: Variants = {
|
||||||
|
initial: { opacity: 0 },
|
||||||
|
animate: { opacity: 1 },
|
||||||
|
exit: { opacity: 0 },
|
||||||
|
};
|
||||||
|
|
||||||
|
const RENDERED_SNIPPETS = [
|
||||||
|
SnippetType.REACT,
|
||||||
|
SnippetType.VUE,
|
||||||
|
SnippetType.HTML,
|
||||||
|
SnippetType.FLUTTER,
|
||||||
|
];
|
||||||
|
|
||||||
|
const buttonColor = "#35313D";
|
||||||
|
const successColor = "#1FA647";
|
||||||
|
const disabledColor = "#B7B7B7";
|
||||||
|
|
||||||
|
const DetailFooter = () => {
|
||||||
|
const [entry, setSelectionEntry] = useRecoilState(selectionEntryAtom);
|
||||||
|
|
||||||
|
const weight = useRecoilValue(iconWeightAtom);
|
||||||
|
const size = useRecoilValue(iconSizeAtom);
|
||||||
|
const color = useRecoilValue(iconColorAtom);
|
||||||
|
const isDark = useRecoilValue(isDarkThemeSelector);
|
||||||
|
const [copied, setCopied] = useTransientState<SnippetType | "SVG" | false>(
|
||||||
|
false,
|
||||||
|
2000
|
||||||
|
);
|
||||||
|
const ref = useRef<SVGSVGElement>(null);
|
||||||
|
|
||||||
|
const [snippets, tabs] = useMemo<
|
||||||
|
[Partial<Record<SnippetType, string>>, Tab[]]
|
||||||
|
>(() => {
|
||||||
|
if (!entry) return [{}, []];
|
||||||
|
|
||||||
|
const snippets = getCodeSnippets({
|
||||||
|
displayName: entry?.pascal_name!,
|
||||||
|
name: entry.name,
|
||||||
|
weight,
|
||||||
|
size,
|
||||||
|
color,
|
||||||
|
});
|
||||||
|
|
||||||
|
const snippetButtonStyle: CSSProperties =
|
||||||
|
weight === "duotone"
|
||||||
|
? { color: disabledColor, userSelect: "none" }
|
||||||
|
: { color: buttonColor };
|
||||||
|
|
||||||
|
const tabs = [
|
||||||
|
{
|
||||||
|
header: "Tags",
|
||||||
|
content: (
|
||||||
|
<TagCloud
|
||||||
|
name={entry.name}
|
||||||
|
tags={Array.from(
|
||||||
|
new Set<string>([
|
||||||
|
...entry.categories,
|
||||||
|
...entry.name.split("-"),
|
||||||
|
...entry.tags,
|
||||||
|
])
|
||||||
|
)}
|
||||||
|
isDark={isDark}
|
||||||
|
/>
|
||||||
|
),
|
||||||
|
},
|
||||||
|
].concat(
|
||||||
|
RENDERED_SNIPPETS.map((type) => {
|
||||||
|
const isWeightSupported = supportsWeight({ type, weight });
|
||||||
|
|
||||||
|
return {
|
||||||
|
header: type,
|
||||||
|
content: (
|
||||||
|
<div className="snippet" key={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>
|
||||||
|
),
|
||||||
|
};
|
||||||
|
})
|
||||||
|
);
|
||||||
|
|
||||||
|
return [snippets, tabs];
|
||||||
|
}, [entry, weight, copied, isDark]);
|
||||||
|
|
||||||
|
useHotkeys("esc", () => setSelectionEntry(null));
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (!entry) return;
|
||||||
|
ReactGA.event({
|
||||||
|
category: "Grid",
|
||||||
|
action: "Details",
|
||||||
|
label: entry.name,
|
||||||
|
});
|
||||||
|
}, [entry]);
|
||||||
|
|
||||||
|
const buttonBarStyle: CSSProperties = {
|
||||||
|
color: isDark ? "white" : buttonColor,
|
||||||
|
backgroundColor: "transparent",
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleCopySnippet = (
|
||||||
|
event: React.MouseEvent<HTMLButtonElement, MouseEvent>,
|
||||||
|
type: SnippetType
|
||||||
|
) => {
|
||||||
|
event.currentTarget.blur();
|
||||||
|
if (!entry) return;
|
||||||
|
|
||||||
|
setCopied(type);
|
||||||
|
const data = snippets[type];
|
||||||
|
data && void navigator.clipboard?.writeText(data);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleCopySVG = (
|
||||||
|
event: React.MouseEvent<HTMLButtonElement, MouseEvent>
|
||||||
|
) => {
|
||||||
|
event.currentTarget.blur();
|
||||||
|
if (!entry) return;
|
||||||
|
|
||||||
|
setCopied("SVG");
|
||||||
|
ref.current && void navigator.clipboard?.writeText(ref.current.outerHTML);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleDownloadSVG = (
|
||||||
|
event: React.MouseEvent<HTMLButtonElement, MouseEvent>
|
||||||
|
) => {
|
||||||
|
event.currentTarget.blur();
|
||||||
|
if (!entry) return;
|
||||||
|
if (!ref.current?.outerHTML) return;
|
||||||
|
|
||||||
|
const blob = new Blob([ref.current.outerHTML]);
|
||||||
|
saveAs(
|
||||||
|
blob,
|
||||||
|
`${entry?.name}${weight === "regular" ? "" : `-${weight}`}.svg`
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleDownloadPNG = async (
|
||||||
|
event: React.MouseEvent<HTMLButtonElement, MouseEvent>
|
||||||
|
) => {
|
||||||
|
event.currentTarget.blur();
|
||||||
|
if (!entry) return;
|
||||||
|
if (!ref.current?.outerHTML) return;
|
||||||
|
|
||||||
|
Svg2Png.save(
|
||||||
|
ref.current,
|
||||||
|
`${entry?.name}${weight === "regular" ? "" : `-${weight}`}.png`,
|
||||||
|
{ scaleX: 2.667, scaleY: 2.667 }
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<AnimatePresence initial={false}>
|
||||||
|
{!!entry && (
|
||||||
|
<motion.aside
|
||||||
|
initial="initial"
|
||||||
|
animate="animate"
|
||||||
|
exit="exit"
|
||||||
|
variants={variants}
|
||||||
|
className={`detail-footer card ${isDark ? "dark" : "light"}`}
|
||||||
|
transition={{ duration: 0.1 }}
|
||||||
|
>
|
||||||
|
<div className="detail-preview">
|
||||||
|
<figure>
|
||||||
|
<entry.Icon ref={ref} size={64}></entry.Icon>
|
||||||
|
<figcaption>{entry.name}</figcaption>
|
||||||
|
</figure>
|
||||||
|
<small className="versioning">
|
||||||
|
in ≥ {entry.published_in.toFixed(1)}.0
|
||||||
|
</small>
|
||||||
|
</div>
|
||||||
|
<Tabs tabs={tabs} />
|
||||||
|
<div className="detail-actions">
|
||||||
|
<button style={buttonBarStyle} onClick={handleDownloadPNG}>
|
||||||
|
<Download size={24} color="currentColor" weight="fill" /> Download
|
||||||
|
PNG
|
||||||
|
</button>
|
||||||
|
<button style={buttonBarStyle} onClick={handleDownloadSVG}>
|
||||||
|
<Download size={24} color="currentColor" weight="fill" /> Download
|
||||||
|
SVG
|
||||||
|
</button>
|
||||||
|
<button style={buttonBarStyle} onClick={handleCopySVG}>
|
||||||
|
{copied === "SVG" ? (
|
||||||
|
<CheckCircle size={24} color={successColor} weight="fill" />
|
||||||
|
) : (
|
||||||
|
<Copy size={24} color="currentColor" weight="fill" />
|
||||||
|
)}
|
||||||
|
{copied === "SVG" ? "Copied!" : "Copy SVG"}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</motion.aside>
|
||||||
|
)}
|
||||||
|
</AnimatePresence>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default DetailFooter;
|
||||||
@@ -153,7 +153,8 @@ const DetailsPanel = (props: InfoPanelProps) => {
|
|||||||
className="icon-preview"
|
className="icon-preview"
|
||||||
>
|
>
|
||||||
<Icon ref={ref} color={color} weight={weight} size={192} />
|
<Icon ref={ref} color={color} weight={weight} size={192} />
|
||||||
<p>{name}</p>
|
<p className="name">{name}</p>
|
||||||
|
<p className="versioning">in ≥ {entry.published_in.toFixed(1)}.0</p>
|
||||||
<TagCloud
|
<TagCloud
|
||||||
name={name}
|
name={name}
|
||||||
tags={Array.from(
|
tags={Array.from(
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
.grid-container {
|
.grid-container {
|
||||||
|
position: relative;
|
||||||
padding: 32px 16px;
|
padding: 32px 16px;
|
||||||
min-height: 80vh;
|
/* min-height: 80vh; */
|
||||||
content-visibility: auto;
|
content-visibility: auto;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -103,6 +104,15 @@
|
|||||||
line-height: 16px;
|
line-height: 16px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.icon-preview > p.name {
|
||||||
|
font-size: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.versioning {
|
||||||
|
margin-top: 2px;
|
||||||
|
opacity: 0.6;
|
||||||
|
}
|
||||||
|
|
||||||
.icon-usage {
|
.icon-usage {
|
||||||
flex: 1;
|
flex: 1;
|
||||||
padding: 56px 10% 56px 0;
|
padding: 56px 10% 56px 0;
|
||||||
@@ -201,3 +211,23 @@
|
|||||||
position: relative;
|
position: relative;
|
||||||
top: -96px;
|
top: -96px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
aside.detail-footer {
|
||||||
|
position: sticky;
|
||||||
|
bottom: 16px;
|
||||||
|
margin: auto;
|
||||||
|
max-width: 1120px;
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: 144px 1fr 160px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.detail-preview {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
padding: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.detail-actions {
|
||||||
|
padding: 16px;
|
||||||
|
}
|
||||||
|
|||||||
@@ -11,6 +11,7 @@ import {
|
|||||||
import useGridSpans from "@/hooks/useGridSpans";
|
import useGridSpans from "@/hooks/useGridSpans";
|
||||||
import Notice from "@/components/Notice";
|
import Notice from "@/components/Notice";
|
||||||
|
|
||||||
|
import DetailFooter from "./DetailFooter";
|
||||||
import IconGridItem from "./IconGridItem";
|
import IconGridItem from "./IconGridItem";
|
||||||
import TagCloud from "./TagCloud";
|
import TagCloud from "./TagCloud";
|
||||||
import "./IconGrid.css";
|
import "./IconGrid.css";
|
||||||
@@ -75,6 +76,7 @@ const IconGrid = (_: IconGridProps) => {
|
|||||||
/>
|
/>
|
||||||
))}
|
))}
|
||||||
</motion.div>
|
</motion.div>
|
||||||
|
<DetailFooter />
|
||||||
</div>
|
</div>
|
||||||
</IconContext.Provider>
|
</IconContext.Provider>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -3,7 +3,7 @@ import { useRecoilState } from "recoil";
|
|||||||
import { motion, AnimatePresence } from "framer-motion";
|
import { motion, AnimatePresence } from "framer-motion";
|
||||||
|
|
||||||
import { IconEntry } from "@/lib";
|
import { IconEntry } from "@/lib";
|
||||||
import { iconPreviewOpenAtom } from "@/state/atoms";
|
import { iconPreviewOpenAtom, selectionEntryAtom } from "@/state/atoms";
|
||||||
|
|
||||||
import DetailsPanel from "./DetailsPanel";
|
import DetailsPanel from "./DetailsPanel";
|
||||||
|
|
||||||
@@ -30,15 +30,15 @@ const itemVariants = {
|
|||||||
const IconGridItem = (props: IconGridItemProps) => {
|
const IconGridItem = (props: IconGridItemProps) => {
|
||||||
const { index, originOffset, entry } = props;
|
const { index, originOffset, entry } = props;
|
||||||
const { name, Icon } = entry;
|
const { name, Icon } = entry;
|
||||||
const [open, setOpen] = useRecoilState(iconPreviewOpenAtom);
|
const [selection, setSelectionEntry] = useRecoilState(selectionEntryAtom);
|
||||||
const isOpen = open === name;
|
const isOpen = selection?.name === name;
|
||||||
const isNew = entry.tags.includes("*new*");
|
const isNew = entry.tags.includes("*new*");
|
||||||
const isUpdated = entry.tags.includes("*updated*");
|
const isUpdated = entry.tags.includes("*updated*");
|
||||||
const delayRef = useRef<number>(0);
|
const delayRef = useRef<number>(0);
|
||||||
const offset = useRef({ top: 0, left: 0 });
|
const offset = useRef({ top: 0, left: 0 });
|
||||||
const ref = useRef<any>();
|
const ref = useRef<any>();
|
||||||
|
|
||||||
const handleOpen = () => setOpen(isOpen ? false : name);
|
const handleOpen = () => setSelectionEntry(isOpen ? null : entry);
|
||||||
|
|
||||||
// The measurement for all elements happens in the layoutEffect cycle
|
// The measurement for all elements happens in the layoutEffect cycle
|
||||||
// This ensures that when we calculate distance in the effect cycle
|
// This ensures that when we calculate distance in the effect cycle
|
||||||
@@ -88,9 +88,9 @@ const IconGridItem = (props: IconGridItemProps) => {
|
|||||||
{isUpdated && <span className="badge updated">•</span>}
|
{isUpdated && <span className="badge updated">•</span>}
|
||||||
</p>
|
</p>
|
||||||
</motion.div>
|
</motion.div>
|
||||||
<AnimatePresence initial={false}>
|
{/* <AnimatePresence initial={false}>
|
||||||
{isOpen && <DetailsPanel {...props} />}
|
{isOpen && <DetailsPanel {...props} />}
|
||||||
</AnimatePresence>
|
</AnimatePresence> */}
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
display: flex;
|
display: flex;
|
||||||
flex-wrap: wrap;
|
flex-wrap: wrap;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
padding: 24px;
|
/* padding: 24px; */
|
||||||
}
|
}
|
||||||
|
|
||||||
button.tag-button {
|
button.tag-button {
|
||||||
|
|||||||
32
src/components/Tabs/Tabs.css
Normal file
32
src/components/Tabs/Tabs.css
Normal file
@@ -0,0 +1,32 @@
|
|||||||
|
.tabs {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
border-left: 2px solid rgba(163, 159, 171, 0.1);
|
||||||
|
border-right: 2px solid rgba(163, 159, 171, 0.1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.tabs-header {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 8px;
|
||||||
|
border-bottom: 2px solid rgba(163, 159, 171, 0.1);
|
||||||
|
}
|
||||||
|
|
||||||
|
button.tab {
|
||||||
|
all: unset;
|
||||||
|
padding: 4px;
|
||||||
|
font-size: 12px;
|
||||||
|
text-align: center;
|
||||||
|
cursor: pointer;
|
||||||
|
flex: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tab.active {
|
||||||
|
background-color: rgba(194, 186, 196, 0.25);
|
||||||
|
}
|
||||||
|
|
||||||
|
.tab-content {
|
||||||
|
flex: 1;
|
||||||
|
display: grid;
|
||||||
|
place-items: center;
|
||||||
|
}
|
||||||
34
src/components/Tabs/Tabs.tsx
Normal file
34
src/components/Tabs/Tabs.tsx
Normal file
@@ -0,0 +1,34 @@
|
|||||||
|
import { ReactNode, useState } from "react";
|
||||||
|
|
||||||
|
import "./Tabs.css";
|
||||||
|
|
||||||
|
export type Tab = {
|
||||||
|
header: ReactNode;
|
||||||
|
content: ReactNode;
|
||||||
|
};
|
||||||
|
|
||||||
|
type TabsProps = {
|
||||||
|
tabs: Tab[];
|
||||||
|
};
|
||||||
|
|
||||||
|
const Tabs = ({ tabs }: TabsProps) => {
|
||||||
|
const [activeIndex, setActiveIndex] = useState<number>(0);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="tabs">
|
||||||
|
<div className="tabs-header">
|
||||||
|
{tabs.map((tab, i) => (
|
||||||
|
<button
|
||||||
|
className={`tab ${activeIndex === i ? "active" : ""}`}
|
||||||
|
onClick={() => setActiveIndex(i)}
|
||||||
|
>
|
||||||
|
{tab.header}
|
||||||
|
</button>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
<div className="tab-content">{tabs[activeIndex]?.content}</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default Tabs;
|
||||||
2
src/components/Tabs/index.ts
Normal file
2
src/components/Tabs/index.ts
Normal file
@@ -0,0 +1,2 @@
|
|||||||
|
export { default } from "./Tabs";
|
||||||
|
export type { Tab } from "./Tabs";
|
||||||
@@ -1,27 +1,33 @@
|
|||||||
import { atom } from "recoil";
|
import { atom } from "recoil";
|
||||||
import { IconStyle } from "@phosphor-icons/core";
|
import { IconStyle } from "@phosphor-icons/core";
|
||||||
|
import { IconEntry } from "@/lib";
|
||||||
|
|
||||||
export const searchQueryAtom = atom<string>({
|
export const searchQueryAtom = atom<string>({
|
||||||
key: "searchQueryAtom",
|
key: "searchQuery",
|
||||||
default: "",
|
default: "",
|
||||||
});
|
});
|
||||||
|
|
||||||
export const iconWeightAtom = atom<IconStyle>({
|
export const iconWeightAtom = atom<IconStyle>({
|
||||||
key: "iconWeightAtom",
|
key: "iconWeight",
|
||||||
default: IconStyle.REGULAR,
|
default: IconStyle.REGULAR,
|
||||||
});
|
});
|
||||||
|
|
||||||
export const iconSizeAtom = atom<number>({
|
export const iconSizeAtom = atom<number>({
|
||||||
key: "iconSizeAtom",
|
key: "iconSize",
|
||||||
default: 32,
|
default: 32,
|
||||||
});
|
});
|
||||||
|
|
||||||
export const iconColorAtom = atom<string>({
|
export const iconColorAtom = atom<string>({
|
||||||
key: "iconColorAtom",
|
key: "iconColor",
|
||||||
default: "#000000",
|
default: "#000000",
|
||||||
});
|
});
|
||||||
|
|
||||||
export const iconPreviewOpenAtom = atom<string | false>({
|
export const iconPreviewOpenAtom = atom<string | false>({
|
||||||
key: "iconPreviewOpenAtom",
|
key: "iconPreviewOpen",
|
||||||
default: false,
|
default: false,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
export const selectionEntryAtom = atom<IconEntry | null>({
|
||||||
|
key: "selectionEntry",
|
||||||
|
default: null,
|
||||||
|
});
|
||||||
|
|||||||
@@ -20,7 +20,7 @@ const fuse = new Fuse(icons, {
|
|||||||
});
|
});
|
||||||
|
|
||||||
export const filteredQueryResultsSelector = selector<ReadonlyArray<IconEntry>>({
|
export const filteredQueryResultsSelector = selector<ReadonlyArray<IconEntry>>({
|
||||||
key: "filteredQueryResultsSelector",
|
key: "filteredQueryResults",
|
||||||
get: ({ get }) => {
|
get: ({ get }) => {
|
||||||
const query = get(searchQueryAtom).trim().toLowerCase();
|
const query = get(searchQueryAtom).trim().toLowerCase();
|
||||||
if (!query) return icons;
|
if (!query) return icons;
|
||||||
@@ -36,7 +36,7 @@ type CategorizedIcons = Partial<Record<IconCategory, IconEntry[]>>;
|
|||||||
export const categorizedQueryResultsSelector = selector<
|
export const categorizedQueryResultsSelector = selector<
|
||||||
Readonly<CategorizedIcons>
|
Readonly<CategorizedIcons>
|
||||||
>({
|
>({
|
||||||
key: "categorizedQueryResultsSelector",
|
key: "categorizedQueryResults",
|
||||||
get: ({ get }) => {
|
get: ({ get }) => {
|
||||||
const filteredResults = get(filteredQueryResultsSelector);
|
const filteredResults = get(filteredQueryResultsSelector);
|
||||||
return new Promise((resolve) =>
|
return new Promise((resolve) =>
|
||||||
@@ -57,7 +57,7 @@ export const singleCategoryQueryResultsSelector = selectorFamily<
|
|||||||
ReadonlyArray<IconEntry>,
|
ReadonlyArray<IconEntry>,
|
||||||
IconCategory
|
IconCategory
|
||||||
>({
|
>({
|
||||||
key: "singleCategoryQueryResultsSelector",
|
key: "singleCategoryQueryResults",
|
||||||
get:
|
get:
|
||||||
(category: IconCategory) =>
|
(category: IconCategory) =>
|
||||||
({ get }) => {
|
({ get }) => {
|
||||||
@@ -71,7 +71,7 @@ export const singleCategoryQueryResultsSelector = selectorFamily<
|
|||||||
});
|
});
|
||||||
|
|
||||||
export const isDarkThemeSelector = selector<boolean>({
|
export const isDarkThemeSelector = selector<boolean>({
|
||||||
key: "isDarkThemeSelector",
|
key: "isDarkTheme",
|
||||||
get: ({ get }) => TinyColor(get(iconColorAtom)).isLight(),
|
get: ({ get }) => TinyColor(get(iconColorAtom)).isLight(),
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user