Using the Fuse package, we now support fuzzy icon search. Results are weighted in favor of icon names and sorted by match score, improving search utility by surfacing best matches to the top of the list. There is still some fine-tuning to do, as threshold often matches unrelated strings, while missing more related but less-similar string queries. In future, we should play with the threshold, location, distance, and possibly the extendedSearch options.
68 lines
2.0 KiB
TypeScript
68 lines
2.0 KiB
TypeScript
import { selector, selectorFamily } from "recoil";
|
|
import TinyColor from "tinycolor2";
|
|
import Fuse from "fuse.js";
|
|
|
|
import { searchQueryAtom, iconColorAtom } from "./atoms";
|
|
import { IconEntry, IconCategory } from "../lib";
|
|
import { icons } from "../lib/icons";
|
|
|
|
const fuse = new Fuse(icons, {
|
|
keys: [{ name: "name", weight: 2 }, "tags", "categories"],
|
|
threshold: 0.2, // Tweak this to what feels like the right number of results
|
|
// shouldSort: false, // We may want to sort if we find too many results?
|
|
});
|
|
|
|
export const filteredQueryResultsSelector = selector<Readonly<IconEntry[]>>({
|
|
key: "filteredQueryResultsSelector",
|
|
get: ({ get }) => {
|
|
const query = get(searchQueryAtom).trim().toLowerCase();
|
|
if (!query) return icons;
|
|
|
|
return new Promise((resolve) =>
|
|
resolve(fuse.search(query).map((value) => value.item))
|
|
);
|
|
},
|
|
});
|
|
|
|
type CategorizedIcons = Partial<Record<IconCategory, IconEntry[]>>;
|
|
|
|
export const categorizedQueryResultsSelector = selector<
|
|
Readonly<CategorizedIcons>
|
|
>({
|
|
key: "categorizedQueryResultsSelector",
|
|
get: ({ get }) => {
|
|
const filteredResults = get(filteredQueryResultsSelector);
|
|
return new Promise((resolve) =>
|
|
resolve(
|
|
filteredResults.reduce<CategorizedIcons>((acc, curr) => {
|
|
curr.categories.forEach((category) => {
|
|
if (!acc[category]) acc[category] = [];
|
|
acc[category]!!.push(curr);
|
|
});
|
|
return acc;
|
|
}, {})
|
|
)
|
|
);
|
|
},
|
|
});
|
|
|
|
export const singleCategoryQueryResultsSelector = selectorFamily<
|
|
Readonly<IconEntry[]>,
|
|
IconCategory
|
|
>({
|
|
key: "singleCategoryQueryResultsSelector",
|
|
get: (category: IconCategory) => ({ get }) => {
|
|
const filteredResults = get(filteredQueryResultsSelector);
|
|
return new Promise((resolve) =>
|
|
resolve(
|
|
filteredResults.filter((icon) => icon.categories.includes(category))
|
|
)
|
|
);
|
|
},
|
|
});
|
|
|
|
export const isDarkThemeSelector = selector<boolean>({
|
|
key: "isDarkThemeSelector",
|
|
get: ({ get }) => TinyColor(get(iconColorAtom)).isLight(),
|
|
});
|