Filter panel visual fixes (#105)

* Make filter panel full page height

* Fix filter items text

when the filter items were cutoff, the text would break onto multiple
lines and the count would overlap with the text

---------

Co-authored-by: Brendan Kellam <bshizzle1234@gmail.com>
This commit is contained in:
Konrad Staniszewski 2024-12-03 12:46:42 -08:00 committed by GitHub
parent 7915af8acd
commit ce8232a23c
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
5 changed files with 38 additions and 23 deletions

View file

@ -25,10 +25,14 @@ export const Entry = ({
}, },
onClicked, onClicked,
}: EntryProps) => { }: EntryProps) => {
let countText = count.toString();
if (count > 999) {
countText = "999+";
}
return ( return (
<div <div
className={clsx("flex flex-row items-center justify-between py-0.5 px-2 cursor-pointer rounded-md gap-2 select-none", className={clsx(
"flex flex-row items-center justify-between py-0.5 px-2 cursor-pointer rounded-md gap-2 select-none",
{ {
"hover:bg-gray-200 dark:hover:bg-gray-700": !isSelected, "hover:bg-gray-200 dark:hover:bg-gray-700": !isSelected,
"bg-blue-200 dark:bg-blue-400": isSelected, "bg-blue-200 dark:bg-blue-400": isSelected,
@ -36,13 +40,15 @@ export const Entry = ({
)} )}
onClick={() => onClicked()} onClick={() => onClicked()}
> >
<div className="flex flex-row items-center gap-1"> <div className="flex flex-row items-center gap-1 overflow-hidden">
{Icon ? Icon : ( {Icon ? Icon : (
<QuestionMarkCircledIcon className="w-4 h-4 flex-shrink-0" /> <QuestionMarkCircledIcon className="w-4 h-4 flex-shrink-0" />
)} )}
<p className="text-wrap">{displayName}</p> <p className="overflow-hidden text-ellipsis whitespace-nowrap">{displayName}</p>
</div>
<div className="px-2 py-0.5 bg-gray-100 dark:bg-gray-800 text-sm rounded-md">
{countText}
</div> </div>
<p>{count}</p>
</div> </div>
); );
} }
@ -53,4 +59,4 @@ export const compareEntries = (a: Entry, b: Entry) => {
} }
return a.count - b.count; return a.count - b.count;
} }

View file

@ -5,12 +5,14 @@ import { compareEntries, Entry } from "./entry";
import { Input } from "@/components/ui/input"; import { Input } from "@/components/ui/input";
import { ScrollArea } from "@/components/ui/scroll-area"; import { ScrollArea } from "@/components/ui/scroll-area";
import Fuse from "fuse.js"; import Fuse from "fuse.js";
import { cn } from "@/lib/utils"
interface FilterProps { interface FilterProps {
title: string, title: string,
searchPlaceholder: string, searchPlaceholder: string,
entries: Entry[], entries: Entry[],
onEntryClicked: (key: string) => void, onEntryClicked: (key: string) => void,
className?: string,
} }
export const Filter = ({ export const Filter = ({
@ -18,6 +20,7 @@ export const Filter = ({
searchPlaceholder, searchPlaceholder,
entries, entries,
onEntryClicked, onEntryClicked,
className,
}: FilterProps) => { }: FilterProps) => {
const [searchFilter, setSearchFilter] = useState<string>(""); const [searchFilter, setSearchFilter] = useState<string>("");
@ -36,7 +39,10 @@ export const Filter = ({
}, [entries, searchFilter]); }, [entries, searchFilter]);
return ( return (
<div className="flex flex-col gap-2 p-1"> <div className={cn(
"flex flex-col gap-2 p-1",
className
)}>
<h2 className="text-sm font-semibold">{title}</h2> <h2 className="text-sm font-semibold">{title}</h2>
<Input <Input
placeholder={searchPlaceholder} placeholder={searchPlaceholder}
@ -44,11 +50,9 @@ export const Filter = ({
onChange={(event) => setSearchFilter(event.target.value)} onChange={(event) => setSearchFilter(event.target.value)}
/> />
<ScrollArea <ScrollArea>
className="overflow-hidden"
>
<div <div
className="flex flex-col gap-0.5 text-sm h-full max-h-80 px-0.5" className="flex flex-col gap-0.5 text-sm px-0.5"
> >
{filteredEntries {filteredEntries
.sort((entryA, entryB) => compareEntries(entryB, entryA)) .sort((entryA, entryB) => compareEntries(entryB, entryA))
@ -63,4 +67,4 @@ export const Filter = ({
</ScrollArea> </ScrollArea>
</div> </div>
) )
} }

View file

@ -118,22 +118,23 @@ export const FilterPanel = ({
onFilterChanged(filteredMatches); onFilterChanged(filteredMatches);
}, [matches, repos, languages, onFilterChanged]); }, [matches, repos, languages, onFilterChanged]);
const numRepos = Object.keys(repos).length > 100 ? '100+' : Object.keys(repos).length;
const numLanguages = Object.keys(languages).length > 100 ? '100+' : Object.keys(languages).length;
return ( return (
<div className="p-3 flex flex-col gap-3"> <div className="p-3 flex flex-col gap-3 h-full">
<h1 className="text-lg font-semibold">Filter Results</h1>
<Filter <Filter
title="By Repository" title="Filter By Repository"
searchPlaceholder="Filter repositories" searchPlaceholder={`Filter ${numRepos} repositories`}
entries={Object.values(repos)} entries={Object.values(repos)}
onEntryClicked={(key) => onEntryClicked(key, setRepos)} onEntryClicked={(key) => onEntryClicked(key, setRepos)}
className="max-h-[50%]"
/> />
<Filter <Filter
title="By Language" title="Filter By Language"
searchPlaceholder="Filter languages" searchPlaceholder={`Filter ${numLanguages} languages`}
entries={Object.values(languages)} entries={Object.values(languages)}
onEntryClicked={(key) => onEntryClicked(key, setLanguages)} onEntryClicked={(key) => onEntryClicked(key, setLanguages)}
className="overflow-auto"
/> />
</div> </div>
) )
@ -167,4 +168,4 @@ const aggregateMatches = (
aggregation[key].count += 1; aggregation[key].count += 1;
return aggregation; return aggregation;
}, {} as Record<string, Entry>) }, {} as Record<string, Entry>)
} }

View file

@ -286,6 +286,7 @@ const PanelGroup = ({
return ( return (
<ResizablePanelGroup <ResizablePanelGroup
direction="horizontal" direction="horizontal"
className="h-full"
> >
{/* ~~ Filter panel ~~ */} {/* ~~ Filter panel ~~ */}
<ResizablePanel <ResizablePanel
@ -354,4 +355,4 @@ const PanelGroup = ({
</ResizablePanel> </ResizablePanel>
</ResizablePanelGroup> </ResizablePanelGroup>
) )
} }

View file

@ -29,7 +29,10 @@ const ResizableHandle = ({
}) => ( }) => (
<ResizablePrimitive.PanelResizeHandle <ResizablePrimitive.PanelResizeHandle
className={cn( className={cn(
"relative flex w-px items-center justify-center bg-border after:absolute after:inset-y-0 after:left-1/2 after:w-1 after:-translate-x-1/2 focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring focus-visible:ring-offset-1 data-[panel-group-direction=vertical]:h-px data-[panel-group-direction=vertical]:w-full data-[panel-group-direction=vertical]:after:left-0 data-[panel-group-direction=vertical]:after:h-1 data-[panel-group-direction=vertical]:after:w-full data-[panel-group-direction=vertical]:after:-translate-y-1/2 data-[panel-group-direction=vertical]:after:translate-x-0 [&[data-panel-group-direction=vertical]>div]:rotate-90", "relative flex w-px items-center justify-center bg-border",
"after:absolute after:inset-y-0 after:left-1/2 after:w-1 after:-translate-x-1/2",
"focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring focus-visible:ring-offset-1",
"data-[panel-group-direction=vertical]:h-px data-[panel-group-direction=vertical]:w-full data-[panel-group-direction=vertical]:after:left-0 data-[panel-group-direction=vertical]:after:h-1 data-[panel-group-direction=vertical]:after:w-full data-[panel-group-direction=vertical]:after:-translate-y-1/2 data-[panel-group-direction=vertical]:after:translate-x-0 [&[data-panel-group-direction=vertical]>div]:rotate-90",
className className
)} )}
{...props} {...props}