Files
Reactive-Resume/src/components/input/icon-picker.tsx
T
Amruth Pillai 5cd16a62d9 v5.0.12 (#2814)
* refactor to @base-ui/react

* fix all

* fixes to accordion

* more updates

* switch to chat/completions api from openai

* update version to v5.0.12
2026-03-17 23:38:06 +01:00

120 lines
3.2 KiB
TypeScript

import { t } from "@lingui/core/macro";
import { ProhibitIcon } from "@phosphor-icons/react";
import Fuse from "fuse.js";
import { memo, useCallback, useMemo, useState } from "react";
import { type CellComponentProps, Grid } from "react-window";
import { Button } from "@/components/ui/button";
import { type IconName, icons } from "@/schema/icons";
import { cn } from "@/utils/style";
import { Input } from "../ui/input";
import { Popover, PopoverContent, PopoverTrigger } from "../ui/popover";
const columnCount = 8;
const columnWidth = 36;
const rowHeight = 36;
type IconSearchInputProps = {
value: string;
onChange: (value: string) => void;
className?: string;
};
function _IconSearchInput(props: IconSearchInputProps) {
return (
<Input
autoFocus
spellCheck={false}
inputMode="search"
value={props.value}
aria-label={t`Search for an icon`}
placeholder={t`Search for an icon`}
onChange={(e) => props.onChange(e.currentTarget.value)}
className={cn("rounded-none border-0 focus-visible:ring-0", props.className)}
/>
);
}
const IconSearchInput = memo(_IconSearchInput);
IconSearchInput.displayName = "IconSearchInput";
type IconCellComponentProps = CellComponentProps & {
icons: IconName[];
onChange: (icon: IconName) => void;
};
function IconCellComponent({ columnIndex, rowIndex, style, icons, onChange }: IconCellComponentProps) {
const index = rowIndex * columnCount + columnIndex;
const icon = icons[index];
return (
<button
type="button"
title={icon}
style={style}
tabIndex={-1}
onClick={() => onChange(icon)}
className="flex size-full items-center justify-center hover:bg-accent"
>
{icon ? <i className={cn("ph text-base", `ph-${icon}`)} /> : <ProhibitIcon />}
</button>
);
}
function useIconSearch() {
const fuse = useMemo(() => new Fuse(icons, { threshold: 0.35 }), []);
const search = useCallback(
(query: string): IconName[] => {
if (!query.trim()) return Array.from(icons);
return fuse.search(query).map((result) => result.item);
},
[fuse],
);
return search;
}
type IconPickerProps = Omit<React.ComponentProps<typeof Button>, "value" | "onChange"> & {
value: string;
onChange: (icon: string) => void;
popoverProps?: React.ComponentProps<typeof Popover>;
};
export function IconPicker({ value, onChange, popoverProps, ...props }: IconPickerProps) {
const searchIcons = useIconSearch();
const [search, setSearch] = useState("");
const searchedIcons = useMemo(() => searchIcons(search), [search, searchIcons]);
const rowCount = useMemo(() => Math.ceil(searchedIcons.length / columnCount), [searchedIcons]);
return (
<Popover {...popoverProps}>
<PopoverTrigger
render={
<Button size="icon" variant="outline" {...props}>
<i className={cn("ph size-4 text-base", `ph-${value}`)} />
</Button>
}
/>
<PopoverContent align="start" className="h-[326px] w-[290px] gap-0 p-0">
<IconSearchInput value={search} onChange={setSearch} />
<div className="size-[290px]">
<Grid
key={search}
rowCount={rowCount}
rowHeight={rowHeight}
columnCount={columnCount}
columnWidth={columnWidth}
cellComponent={IconCellComponent}
cellProps={{ icons: searchedIcons, onChange }}
/>
</div>
</PopoverContent>
</Popover>
);
}