Fix component bugs across hooks, elements, form, layout, editors and utils

- Hooks: per-instance timers in useReady; cleanup for useIntersectionObserver;
  useWebSocket no longer reconnects after unmount/refresh, drains the queue
  and dispatches non-JSON messages
- Modal/Popover/Dropdown: onClose only on close, Escape to close, no stray
  DOM props, hooks before early return, scrollbar-aware positioning,
  top-left/top-right dropdown positions
- Elements: Toast/Search/StarRating per-instance timers, Pagination labels
  and empty state, Breadcrumbs titles, LinkList dividers/props, segment-wise
  active-link matching (new utils/link-matches-path), Table object cells
- Form: Select with a default option is changeable, Input clear notifies
  controlled parents and copy works on textareas, Checkbox random label,
  NumberInputButtons runaway interval, upload prop/spread fixes
- Layout: Button disabled while loading, Stack gap and Span truncate via
  inline styles, Img fallbackImageSrc, className/JSDoc fixes
- Editors/MDX/AI: TinyMCE id/cleanup/stale handler/duplicate scripts,
  AceEditor debounce, markdown heights and frontmatter regex, last
  assistant message, AIPromptBlock prompt ref
- Utils: fetchApi headers/FormData/fetchOptions/no mutation, base64 file
  names and read errors, slugify underscore trim, serializeQuery zero values
- Tests for fetchApi, link matching, slugify and serializeQuery
- AGENTS.md updated for the package layout

Co-Authored-By: Claude Opus 5.5 <[email protected]>
This commit is contained in:
2026-10-18 10:45:46 +01:00
co-authored by Claude Opus 5.5
parent 11da9b89e7
commit cdeeb8201e
56 changed files with 1128 additions and 722 deletions
+7
View File
@@ -59,6 +59,7 @@ export default function NumberInputButtons({
function incrementDownPress() {
window.clearTimeout(pressTimeout);
window.clearInterval(pressInterval);
setButtonDown(true);
pressTimeout = setTimeout(() => {
@@ -75,6 +76,8 @@ export default function NumberInputButtons({
}
function decrementDownPress() {
window.clearTimeout(pressTimeout);
window.clearInterval(pressInterval);
setButtonDown(true);
pressTimeout = setTimeout(() => {
@@ -143,7 +146,9 @@ export default function NumberInputButtons({
onMouseDown={decrementDownPress}
onTouchStart={decrementDownPress}
onMouseUp={decrementDownCancel}
onMouseLeave={decrementDownCancel}
onTouchEnd={decrementDownCancel}
onTouchCancel={decrementDownCancel}
>
<Minus size={20} />
</Row>
@@ -156,7 +161,9 @@ export default function NumberInputButtons({
onMouseDown={incrementDownPress}
onTouchStart={incrementDownPress}
onMouseUp={incrementDownCancel}
onMouseLeave={incrementDownCancel}
onTouchEnd={incrementDownCancel}
onTouchCancel={incrementDownCancel}
>
<Plus size={20} />
</Row>
+37 -28
View File
@@ -153,8 +153,10 @@ export default function Input<KeyType extends string>(
},
);
const inputRef = componentRef || React.useRef<HTMLInputElement>(null);
const textAreaRef = componentRef || React.useRef<HTMLTextAreaElement>(null);
const internalInputRef = React.useRef<HTMLInputElement>(null);
const internalTextAreaRef = React.useRef<HTMLTextAreaElement>(null);
const inputRef = componentRef || internalInputRef;
const textAreaRef = componentRef || internalTextAreaRef;
const buttonDownRef = React.useRef(false);
const [value, setValue] = React.useState(
@@ -344,7 +346,11 @@ export default function Input<KeyType extends string>(
return (
<Stack
title={`${finalLabel}${props.required ? " (Required)" : ""}`}
title={
finalLabel
? `${finalLabel}${props.required ? " (Required)" : ""}`
: undefined
}
{...wrapperWrapperProps}
className={twMerge(
"w-full gap-1.5 relative z-0 hover:z-100",
@@ -417,9 +423,9 @@ export default function Input<KeyType extends string>(
title="Copy value"
className="cursor-pointer opacity-50 hover:opacity-100"
onClick={() => {
const text =
inputRef.current.value ||
textAreaRef.current.value;
const text = (
istextarea ? textAreaRef : inputRef
).current?.value;
if (text) {
navigator.clipboard
@@ -446,14 +452,18 @@ export default function Input<KeyType extends string>(
onClick={(e) => {
e.preventDefault();
if (inputRef.current) {
inputRef.current.value = "";
}
if (textAreaRef.current) {
textAreaRef.current.value = "";
const element = istextarea
? textAreaRef.current
: inputRef.current;
// Goes through React's onChange so controlled
// inputs and `onChange` listeners see the clear
if (element) {
setNativeInputValue(element, "");
} else {
setValue("");
}
setValue("");
clearInputProps?.onClick?.(e);
}}
>
@@ -465,7 +475,7 @@ export default function Input<KeyType extends string>(
<div
title={
inputType == "password"
? "View Psasword"
? "View Password"
: "Hide Password"
}
className={twMerge("p-1 -my-2 -mx-1")}
@@ -545,25 +555,24 @@ export default function Input<KeyType extends string>(
const PICKER_INPUT_TYPES = ["date", "datetime-local", "time", "month", "week"];
const updateInput = ({
newValue,
inputRef,
}: {
newValue: string;
inputRef: RefObject<HTMLInputElement & HTMLTextAreaElement>;
}) => {
const input = inputRef.current;
if (!input) return;
function setNativeInputValue(
element: HTMLInputElement | HTMLTextAreaElement,
newValue: string,
) {
const prototype =
element instanceof HTMLTextAreaElement
? window.HTMLTextAreaElement.prototype
: window.HTMLInputElement.prototype;
// Get the native setter (React overrides the default one)
const nativeInputValueSetter = Object.getOwnPropertyDescriptor(
window.HTMLInputElement.prototype,
const nativeValueSetter = Object.getOwnPropertyDescriptor(
prototype,
"value",
)?.set;
// Call it directly on the input
nativeInputValueSetter?.call(input, newValue);
// Call it directly on the element
nativeValueSetter?.call(element, newValue);
// Dispatch the event - React listens for 'input', not 'change'
input.dispatchEvent(new Event("input", { bubbles: true }));
};
element.dispatchEvent(new Event("input", { bubbles: true }));
}