chore: import upstream snapshot with attribution

This commit is contained in:
wehub-resource-sync
2026-07-13 13:32:57 +08:00
commit cd420f9332
4811 changed files with 884702 additions and 0 deletions
@@ -0,0 +1,178 @@
import { MagnifyingGlassIcon, XMarkIcon } from "@heroicons/react/20/solid";
import { motion } from "framer-motion";
import { useEffect, useRef, useState } from "react";
import { Input } from "~/components/primitives/Input";
import { ShortcutKey } from "~/components/primitives/ShortcutKey";
import { SimpleTooltip } from "~/components/primitives/Tooltip";
import { useSearchParams } from "~/hooks/useSearchParam";
import { cn } from "~/utils/cn";
export type SearchInputProps = {
placeholder?: string;
/** The URL search param name to read/write. Defaults to "search". */
paramName?: string;
/** Additional URL params to reset when searching or clearing (e.g. pagination). Defaults to ["cursor", "direction"]. */
resetParams?: string[];
autoFocus?: boolean;
/**
* Controlled value. When provided alongside `onValueChange`, the input
* skips URL params entirely and acts as a controlled component — useful
* for client-side filters (e.g. tree filtering) that don't round-trip
* to the server.
*/
value?: string;
/**
* Called on every keystroke and on clear when in controlled mode. The
* presence of this prop is what switches the input into controlled
* mode; `paramName`/`resetParams` are ignored when it's set.
*/
onValueChange?: (value: string) => void;
};
export function SearchInput({
placeholder = "Search logs…",
paramName = "search",
resetParams = ["cursor", "direction"],
autoFocus,
value: controlledValue,
onValueChange,
}: SearchInputProps) {
const inputRef = useRef<HTMLInputElement>(null);
const { value, replace, del } = useSearchParams();
const isControlled = onValueChange !== undefined;
const initialSearch = isControlled ? (controlledValue ?? "") : (value(paramName) ?? "");
const [text, setText] = useState(initialSearch);
const [isFocused, setIsFocused] = useState(false);
// Compare against a ref, not `text`, so the effect stays off the keystroke path.
// Trade-off: controlled mode assumes the parent accepts onValueChange; it won't
// re-sync `text` if the parent rejects a change and holds `value` unchanged.
const lastSyncedRef = useRef(initialSearch);
useEffect(() => {
if (isControlled) {
if (controlledValue !== undefined && controlledValue !== lastSyncedRef.current) {
lastSyncedRef.current = controlledValue;
setText(controlledValue);
}
return;
}
const urlSearch = value(paramName) ?? "";
if (urlSearch === lastSyncedRef.current) return;
// Only mark synced once we actually apply it, so a URL change during focus still syncs on blur.
if (!isFocused) {
lastSyncedRef.current = urlSearch;
setText(urlSearch);
}
}, [isControlled, controlledValue, value, isFocused, paramName]);
const updateText = (next: string) => {
setText(next);
if (isControlled) {
onValueChange?.(next);
}
};
const handleSubmit = () => {
if (isControlled) {
// Live updates already fired through onValueChange; submit is a no-op.
return;
}
const resetValues = Object.fromEntries(resetParams.map((p) => [p, undefined]));
if (text.trim()) {
replace({ [paramName]: text.trim(), ...resetValues });
} else {
del([paramName, ...resetParams]);
}
};
const handleClear = () => {
setText("");
if (isControlled) {
onValueChange?.("");
} else {
del([paramName, ...resetParams]);
}
};
return (
<div className="flex items-center gap-1">
<motion.div
initial={{ width: "auto" }}
animate={{ width: isFocused && text.length > 0 ? "24rem" : "auto" }}
transition={{
type: "spring",
stiffness: 300,
damping: 30,
}}
className="relative h-6 min-w-52"
>
<Input
type="text"
ref={inputRef}
variant="secondary-small"
placeholder={placeholder}
value={text}
onChange={(e) => updateText(e.target.value)}
fullWidth
autoFocus={autoFocus}
className={cn("", isFocused && "placeholder:text-text-dimmed/70")}
onKeyDown={(e) => {
if (e.key === "Enter") {
e.preventDefault();
handleSubmit();
}
if (e.key === "Escape") {
if (text.length > 0) {
e.stopPropagation();
handleClear();
} else {
e.currentTarget.blur();
}
}
}}
onFocus={() => setIsFocused(true)}
onBlur={() => setIsFocused(false)}
icon={<MagnifyingGlassIcon className="size-4 text-text-bright" />}
accessory={
text.length > 0 ? (
<div className="-mr-1 flex items-center gap-1.5">
{!isControlled && (
<ShortcutKey
shortcut={{ key: "enter" }}
variant="medium"
className="border-none"
/>
)}
<SimpleTooltip
asChild
button={
<button
type="button"
onMouseDown={(e) => e.preventDefault()}
onClick={() => handleClear()}
className="flex size-4.5 items-center justify-center rounded-[2px] border border-text-dimmed/40 text-text-dimmed transition hover:bg-surface-control hover:text-text-bright"
>
<XMarkIcon className="size-3" />
</button>
}
content={
<div className="flex items-center gap-1">
<span className="text-text-dimmed">Clear field</span>
<ShortcutKey shortcut={{ key: "esc" }} variant="small" />
</div>
}
className="px-2 py-1.5 text-xs"
disableHoverableContent
/>
</div>
) : undefined
}
/>
</motion.div>
</div>
);
}