feat: add expandable filter chips

This commit is contained in:
TerryM
2026-05-27 12:23:17 +08:00
parent 54841a4ed9
commit 8120f6b05c

View File

@@ -1,3 +1,5 @@
import { SlidersHorizontal } from "lucide-react";
import { useEffect, useMemo, useRef, useState } from "react";
import { useI18n } from "../../i18n"; import { useI18n } from "../../i18n";
import { typeFilterLabel } from "../../resourceTypeLabels"; import { typeFilterLabel } from "../../resourceTypeLabels";
@@ -20,9 +22,54 @@ export type FilterChipsProps = {
export function FilterChips({ type, onTypeChange }: FilterChipsProps) { export function FilterChips({ type, onTypeChange }: FilterChipsProps) {
const { t } = useI18n(); const { t } = useI18n();
const containerRef = useRef<HTMLDivElement>(null);
const measureRef = useRef<HTMLDivElement>(null);
const [expanded, setExpanded] = useState(false);
const [hasOverflow, setHasOverflow] = useState(false);
const labelsKey = useMemo(
() => TYPE_FILTERS.map((tp) => typeFilterLabel(t, tp)).join("|"),
[t],
);
useEffect(() => {
const checkOverflow = () => {
const container = containerRef.current;
const measure = measureRef.current;
if (!container || !measure) return;
const nextHasOverflow = measure.scrollWidth > container.clientWidth + 1;
setHasOverflow(nextHasOverflow);
if (!nextHasOverflow) setExpanded(false);
};
checkOverflow();
const resizeObserver = new ResizeObserver(checkOverflow);
if (containerRef.current) resizeObserver.observe(containerRef.current);
if (measureRef.current) resizeObserver.observe(measureRef.current);
return () => resizeObserver.disconnect();
}, [labelsKey]);
const chipClass = (active: boolean) =>
`inline-flex h-8 min-w-[72px] shrink-0 items-center justify-center rounded-full border px-3 text-xs leading-none transition ${
active
? "border-ark-gold bg-ark-gold/10 text-ark-gold2"
: "border-ark-line text-neutral-300 hover:border-ark-gold/50"
}`;
return ( return (
<div className="sticky top-0 z-10 border-b border-ark-line bg-ark-bg/90 py-2 backdrop-blur md:rounded-t-xl"> <div
<div className="flex gap-1.5 overflow-x-auto whitespace-nowrap [-ms-overflow-style:none] [scrollbar-width:none] [&::-webkit-scrollbar]:hidden"> ref={containerRef}
className="sticky top-0 z-10 border-b border-ark-line bg-ark-bg/90 py-2 backdrop-blur md:rounded-t-xl"
>
<div className="flex items-start gap-1.5">
<div
className={`flex min-w-0 flex-1 gap-1.5 ${
expanded
? "flex-wrap whitespace-normal"
: "overflow-hidden whitespace-nowrap"
}`}
>
{TYPE_FILTERS.map((tp) => { {TYPE_FILTERS.map((tp) => {
const active = type === tp; const active = type === tp;
return ( return (
@@ -30,17 +77,41 @@ export function FilterChips({ type, onTypeChange }: FilterChipsProps) {
key={tp} key={tp}
type="button" type="button"
onClick={() => onTypeChange(tp)} onClick={() => onTypeChange(tp)}
className={`shrink-0 rounded-full border px-3 py-1 text-xs transition ${ className={chipClass(active)}
active
? "border-ark-gold bg-ark-gold/10 text-ark-gold2"
: "border-ark-line text-neutral-300 hover:border-ark-gold/50"
}`}
> >
{typeFilterLabel(t, tp)} {typeFilterLabel(t, tp)}
</button> </button>
); );
})} })}
</div> </div>
{hasOverflow ? (
<button
type="button"
onClick={() => setExpanded((value) => !value)}
className="inline-flex h-8 w-8 shrink-0 items-center justify-center rounded-full border border-ark-line bg-[#1a1b20] text-neutral-200 transition hover:border-ark-gold/60 hover:text-ark-gold2 min-[440px]:w-9 md:w-10"
aria-label={expanded ? "Collapse filters" : "Expand filters"}
aria-expanded={expanded}
>
<SlidersHorizontal
className="h-3.5 w-3.5 md:h-4 md:w-4"
strokeWidth={2.2}
/>
</button>
) : null}
</div>
<div
ref={measureRef}
aria-hidden="true"
className="pointer-events-none invisible absolute left-0 top-0 -z-10 flex gap-1.5 whitespace-nowrap"
>
{TYPE_FILTERS.map((tp) => (
<span key={tp} className={chipClass(type === tp)}>
{typeFilterLabel(t, tp)}
</span>
))}
</div>
</div> </div>
); );
} }