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:
+231
-228
@@ -1,228 +1,231 @@
|
||||
import React, { type ComponentProps, type ReactNode } from "react";
|
||||
import Link from "../layout/Link";
|
||||
import Divider from "../layout/Divider";
|
||||
import Row from "../layout/Row";
|
||||
import lowerToTitleCase from "../utils/lower-to-title-case";
|
||||
import { twMerge } from "tailwind-merge";
|
||||
import { ChevronLeft } from "lucide-react";
|
||||
import Button from "../layout/Button";
|
||||
|
||||
type LinkObject = {
|
||||
title: string;
|
||||
path: string;
|
||||
};
|
||||
|
||||
type Props = {
|
||||
excludeRegexMatch?: RegExp;
|
||||
linkProps?: ComponentProps<typeof Link>;
|
||||
currentLinkProps?: ComponentProps<typeof Link>;
|
||||
dividerProps?: ComponentProps<typeof Divider>;
|
||||
backButtonProps?: ComponentProps<typeof Button>;
|
||||
backButton?: boolean;
|
||||
pageUrl?: string;
|
||||
currentTitle?: string;
|
||||
skipHome?: boolean;
|
||||
divider?: ReactNode;
|
||||
};
|
||||
|
||||
/**
|
||||
* # TWUI Breadcrumbs
|
||||
* @className `twui-breadcrumb-link`
|
||||
* @className `twui-current-breadcrumb-wrapper`
|
||||
* @className `twui-breadcrumbs-divider`
|
||||
* @className `twui-breadcrumbs-back-button`
|
||||
*/
|
||||
export default function Breadcrumbs({
|
||||
excludeRegexMatch,
|
||||
linkProps,
|
||||
currentLinkProps,
|
||||
dividerProps,
|
||||
backButton,
|
||||
backButtonProps,
|
||||
pageUrl,
|
||||
currentTitle,
|
||||
skipHome,
|
||||
divider,
|
||||
}: Props) {
|
||||
const [links, setLinks] = React.useState<LinkObject[] | null>(
|
||||
pageUrl
|
||||
? twuiBreadcrumbsGenerateLinksFromUrl({ url: pageUrl, skipHome })
|
||||
: null,
|
||||
);
|
||||
|
||||
React.useEffect(() => {
|
||||
if (links) return;
|
||||
|
||||
let pathname = window.location.pathname;
|
||||
|
||||
let validPathLinks = twuiBreadcrumbsGenerateLinksFromUrl({
|
||||
url: pathname,
|
||||
excludeRegexMatch,
|
||||
skipHome,
|
||||
});
|
||||
|
||||
setLinks(validPathLinks);
|
||||
|
||||
return function () {
|
||||
setLinks(null);
|
||||
};
|
||||
}, []);
|
||||
|
||||
if (!links?.[1]) {
|
||||
return <React.Fragment></React.Fragment>;
|
||||
}
|
||||
|
||||
return (
|
||||
<nav
|
||||
className={twMerge(
|
||||
"overflow-x-auto",
|
||||
"twui-current-breadcrumb-wrapper",
|
||||
)}
|
||||
aria-label="Breadcrumb"
|
||||
>
|
||||
<Row
|
||||
className={twMerge(
|
||||
"gap-4 flex-nowrap whitespace-nowrap overflow-x-auto overflow-y-hidden w-full",
|
||||
)}
|
||||
>
|
||||
{backButton && (
|
||||
<React.Fragment>
|
||||
<Button
|
||||
variant="ghost"
|
||||
color="gray"
|
||||
{...backButtonProps}
|
||||
className={twMerge(
|
||||
"p-1 -my-2 -mx-2",
|
||||
"twui-breadcrumbs-back-button",
|
||||
backButtonProps?.className,
|
||||
)}
|
||||
onClick={(e) => {
|
||||
window.history.back();
|
||||
backButtonProps?.onClick?.(e);
|
||||
}}
|
||||
title="Breadcrumbs Back Button"
|
||||
beforeIcon={<ChevronLeft size={20} />}
|
||||
/>
|
||||
{divider || (
|
||||
<Divider
|
||||
vertical
|
||||
className={twMerge(
|
||||
"twui-breadcrumbs-divider",
|
||||
dividerProps?.className,
|
||||
)}
|
||||
/>
|
||||
)}
|
||||
</React.Fragment>
|
||||
)}
|
||||
|
||||
{links.map((linkObject, index, array) => {
|
||||
const isTarget = array.length - 1 == index;
|
||||
|
||||
if (index === links.length - 1) {
|
||||
return (
|
||||
<Link
|
||||
key={index}
|
||||
href={linkObject.path}
|
||||
{...linkProps}
|
||||
{...(isTarget ? currentLinkProps : {})}
|
||||
className={twMerge(
|
||||
"text-primary-text/50 dark:text-primary-dark-text/50 text-xs",
|
||||
"max-w-[200px] text-ellipsis overflow-hidden",
|
||||
isTarget ? "current" : "",
|
||||
"twui-breadcrumb-link",
|
||||
linkProps?.className,
|
||||
isTarget && currentLinkProps?.className,
|
||||
)}
|
||||
title={
|
||||
currentLinkProps?.title || linkObject.title
|
||||
}
|
||||
>
|
||||
{currentTitle || linkObject.title}
|
||||
</Link>
|
||||
);
|
||||
} else {
|
||||
return (
|
||||
<React.Fragment key={index}>
|
||||
<Link
|
||||
href={linkObject.path}
|
||||
{...linkProps}
|
||||
{...(isTarget ? currentLinkProps : {})}
|
||||
className={twMerge(
|
||||
"text-xs",
|
||||
isTarget ? "current" : "",
|
||||
"twui-breadcrumb-link",
|
||||
linkProps?.className,
|
||||
isTarget && currentLinkProps?.className,
|
||||
)}
|
||||
>
|
||||
{currentLinkProps?.title ||
|
||||
linkObject.title}
|
||||
</Link>
|
||||
{divider || (
|
||||
<Divider
|
||||
vertical
|
||||
{...dividerProps}
|
||||
className={twMerge(
|
||||
"twui-breadcrumbs-divider",
|
||||
dividerProps?.className,
|
||||
)}
|
||||
/>
|
||||
)}
|
||||
</React.Fragment>
|
||||
);
|
||||
}
|
||||
})}
|
||||
</Row>
|
||||
</nav>
|
||||
);
|
||||
////////////////////////////////////////
|
||||
////////////////////////////////////////
|
||||
////////////////////////////////////////
|
||||
}
|
||||
|
||||
export function twuiBreadcrumbsGenerateLinksFromUrl({
|
||||
url,
|
||||
excludeRegexMatch,
|
||||
skipHome,
|
||||
}: {
|
||||
url: string;
|
||||
excludeRegexMatch?: RegExp;
|
||||
skipHome?: boolean;
|
||||
}) {
|
||||
let pathLinks = url.split("/");
|
||||
|
||||
let validPathLinks = [];
|
||||
|
||||
if (!skipHome) {
|
||||
validPathLinks.push({
|
||||
title: "Home",
|
||||
path: url.match(/admin/) ? "/admin" : "/",
|
||||
});
|
||||
}
|
||||
|
||||
pathLinks.forEach((linkText, index, array) => {
|
||||
if (!linkText?.match(/./)) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (excludeRegexMatch && excludeRegexMatch.test(linkText)) return;
|
||||
|
||||
validPathLinks.push({
|
||||
title: lowerToTitleCase(linkText),
|
||||
path: (() => {
|
||||
let path = "";
|
||||
|
||||
for (let i = 0; i < array.length; i++) {
|
||||
const lnText = array[i];
|
||||
if (i > index || !lnText?.match(/./)) continue;
|
||||
|
||||
path += `/${lnText}`;
|
||||
}
|
||||
|
||||
return path;
|
||||
})(),
|
||||
});
|
||||
});
|
||||
|
||||
return validPathLinks;
|
||||
}
|
||||
import React, { type ComponentProps, type ReactNode } from "react";
|
||||
import Link from "../layout/Link";
|
||||
import Divider from "../layout/Divider";
|
||||
import Row from "../layout/Row";
|
||||
import lowerToTitleCase from "../utils/lower-to-title-case";
|
||||
import { twMerge } from "tailwind-merge";
|
||||
import { ChevronLeft } from "lucide-react";
|
||||
import Button from "../layout/Button";
|
||||
|
||||
type LinkObject = {
|
||||
title: string;
|
||||
path: string;
|
||||
};
|
||||
|
||||
type Props = {
|
||||
excludeRegexMatch?: RegExp;
|
||||
linkProps?: ComponentProps<typeof Link>;
|
||||
currentLinkProps?: ComponentProps<typeof Link>;
|
||||
dividerProps?: ComponentProps<typeof Divider>;
|
||||
backButtonProps?: ComponentProps<typeof Button>;
|
||||
backButton?: boolean;
|
||||
pageUrl?: string;
|
||||
currentTitle?: string;
|
||||
skipHome?: boolean;
|
||||
divider?: ReactNode;
|
||||
};
|
||||
|
||||
/**
|
||||
* # TWUI Breadcrumbs
|
||||
* @className `twui-breadcrumb-link`
|
||||
* @className `twui-current-breadcrumb-wrapper`
|
||||
* @className `twui-breadcrumbs-divider`
|
||||
* @className `twui-breadcrumbs-back-button`
|
||||
*/
|
||||
export default function Breadcrumbs({
|
||||
excludeRegexMatch,
|
||||
linkProps,
|
||||
currentLinkProps,
|
||||
dividerProps,
|
||||
backButton,
|
||||
backButtonProps,
|
||||
pageUrl,
|
||||
currentTitle,
|
||||
skipHome,
|
||||
divider,
|
||||
}: Props) {
|
||||
const [links, setLinks] = React.useState<LinkObject[] | null>(
|
||||
pageUrl
|
||||
? twuiBreadcrumbsGenerateLinksFromUrl({
|
||||
url: pageUrl,
|
||||
excludeRegexMatch,
|
||||
skipHome,
|
||||
})
|
||||
: null,
|
||||
);
|
||||
|
||||
React.useEffect(() => {
|
||||
if (links) return;
|
||||
|
||||
let pathname = window.location.pathname;
|
||||
|
||||
let validPathLinks = twuiBreadcrumbsGenerateLinksFromUrl({
|
||||
url: pathname,
|
||||
excludeRegexMatch,
|
||||
skipHome,
|
||||
});
|
||||
|
||||
setLinks(validPathLinks);
|
||||
|
||||
return function () {
|
||||
setLinks(null);
|
||||
};
|
||||
}, []);
|
||||
|
||||
if (!links?.[1]) {
|
||||
return <React.Fragment></React.Fragment>;
|
||||
}
|
||||
|
||||
return (
|
||||
<nav
|
||||
className={twMerge(
|
||||
"overflow-x-auto",
|
||||
"twui-current-breadcrumb-wrapper",
|
||||
)}
|
||||
aria-label="Breadcrumb"
|
||||
>
|
||||
<Row
|
||||
className={twMerge(
|
||||
"gap-4 flex-nowrap whitespace-nowrap overflow-x-auto overflow-y-hidden w-full",
|
||||
)}
|
||||
>
|
||||
{backButton && (
|
||||
<React.Fragment>
|
||||
<Button
|
||||
variant="ghost"
|
||||
color="gray"
|
||||
{...backButtonProps}
|
||||
className={twMerge(
|
||||
"p-1 -my-2 -mx-2",
|
||||
"twui-breadcrumbs-back-button",
|
||||
backButtonProps?.className,
|
||||
)}
|
||||
onClick={(e) => {
|
||||
window.history.back();
|
||||
backButtonProps?.onClick?.(e);
|
||||
}}
|
||||
title="Breadcrumbs Back Button"
|
||||
beforeIcon={<ChevronLeft size={20} />}
|
||||
/>
|
||||
{divider || (
|
||||
<Divider
|
||||
vertical
|
||||
className={twMerge(
|
||||
"twui-breadcrumbs-divider",
|
||||
dividerProps?.className,
|
||||
)}
|
||||
/>
|
||||
)}
|
||||
</React.Fragment>
|
||||
)}
|
||||
|
||||
{links.map((linkObject, index, array) => {
|
||||
const isTarget = array.length - 1 == index;
|
||||
|
||||
if (index === links.length - 1) {
|
||||
return (
|
||||
<Link
|
||||
key={index}
|
||||
href={linkObject.path}
|
||||
{...linkProps}
|
||||
{...(isTarget ? currentLinkProps : {})}
|
||||
className={twMerge(
|
||||
"text-primary-text/50 dark:text-primary-dark-text/50 text-xs",
|
||||
"max-w-[200px] text-ellipsis overflow-hidden",
|
||||
isTarget ? "current" : "",
|
||||
"twui-breadcrumb-link",
|
||||
linkProps?.className,
|
||||
isTarget && currentLinkProps?.className,
|
||||
)}
|
||||
title={
|
||||
currentLinkProps?.title || linkObject.title
|
||||
}
|
||||
>
|
||||
{currentTitle || linkObject.title}
|
||||
</Link>
|
||||
);
|
||||
} else {
|
||||
return (
|
||||
<React.Fragment key={index}>
|
||||
<Link
|
||||
href={linkObject.path}
|
||||
{...linkProps}
|
||||
{...(isTarget ? currentLinkProps : {})}
|
||||
className={twMerge(
|
||||
"text-xs",
|
||||
isTarget ? "current" : "",
|
||||
"twui-breadcrumb-link",
|
||||
linkProps?.className,
|
||||
isTarget && currentLinkProps?.className,
|
||||
)}
|
||||
>
|
||||
{linkObject.title}
|
||||
</Link>
|
||||
{divider || (
|
||||
<Divider
|
||||
vertical
|
||||
{...dividerProps}
|
||||
className={twMerge(
|
||||
"twui-breadcrumbs-divider",
|
||||
dividerProps?.className,
|
||||
)}
|
||||
/>
|
||||
)}
|
||||
</React.Fragment>
|
||||
);
|
||||
}
|
||||
})}
|
||||
</Row>
|
||||
</nav>
|
||||
);
|
||||
////////////////////////////////////////
|
||||
////////////////////////////////////////
|
||||
////////////////////////////////////////
|
||||
}
|
||||
|
||||
export function twuiBreadcrumbsGenerateLinksFromUrl({
|
||||
url,
|
||||
excludeRegexMatch,
|
||||
skipHome,
|
||||
}: {
|
||||
url: string;
|
||||
excludeRegexMatch?: RegExp;
|
||||
skipHome?: boolean;
|
||||
}) {
|
||||
let pathLinks = url.split("/");
|
||||
|
||||
let validPathLinks = [];
|
||||
|
||||
if (!skipHome) {
|
||||
validPathLinks.push({
|
||||
title: "Home",
|
||||
path: url.match(/admin/) ? "/admin" : "/",
|
||||
});
|
||||
}
|
||||
|
||||
pathLinks.forEach((linkText, index, array) => {
|
||||
if (!linkText?.match(/./)) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (excludeRegexMatch && excludeRegexMatch.test(linkText)) return;
|
||||
|
||||
validPathLinks.push({
|
||||
title: lowerToTitleCase(linkText),
|
||||
path: (() => {
|
||||
let path = "";
|
||||
|
||||
for (let i = 0; i < array.length; i++) {
|
||||
const lnText = array[i];
|
||||
if (i > index || !lnText?.match(/./)) continue;
|
||||
|
||||
path += `/${lnText}`;
|
||||
}
|
||||
|
||||
return path;
|
||||
})(),
|
||||
});
|
||||
});
|
||||
|
||||
return validPathLinks;
|
||||
}
|
||||
|
||||
@@ -106,6 +106,11 @@ export default function Dropdown({
|
||||
|
||||
React.useEffect(() => {
|
||||
setMounted(true);
|
||||
|
||||
return () => {
|
||||
window.clearTimeout(timeoutRef.current);
|
||||
window.clearTimeout(openTimeoutRef.current);
|
||||
};
|
||||
}, []);
|
||||
|
||||
/**
|
||||
@@ -146,6 +151,15 @@ export default function Dropdown({
|
||||
case "bottom-right":
|
||||
style = { right: viewportWidth - rect.right, top: rect.bottom };
|
||||
break;
|
||||
case "top-left":
|
||||
style = { left: rect.left, bottom: viewportHeight - rect.top };
|
||||
break;
|
||||
case "top-right":
|
||||
style = {
|
||||
right: viewportWidth - rect.right,
|
||||
bottom: viewportHeight - rect.top,
|
||||
};
|
||||
break;
|
||||
case "top":
|
||||
style = {
|
||||
left: centerX,
|
||||
|
||||
@@ -6,6 +6,7 @@ import React, {
|
||||
import { twMerge } from "tailwind-merge";
|
||||
import Row from "../layout/Row";
|
||||
import HeaderNavLinkComponent from "./HeaderNavLinkComponent";
|
||||
import twuiLinkMatchesPath from "../utils/link-matches-path";
|
||||
|
||||
export type TWUI_HEADER_NAV_PROPS = DetailedHTMLProps<
|
||||
HTMLAttributes<HTMLElement>,
|
||||
@@ -84,17 +85,13 @@ export function twuiAddActiveLinksFn({
|
||||
|
||||
const linkAttr = linkEl.getAttribute("href");
|
||||
|
||||
if (window.location.pathname === "/" && linkAttr == "/") {
|
||||
linkEl.classList.add("active");
|
||||
} else if (
|
||||
isLinkStrict &&
|
||||
linkEl.getAttribute("href") === window.location.pathname
|
||||
) {
|
||||
linkEl.classList.add("active");
|
||||
} else if (
|
||||
if (
|
||||
linkAttr &&
|
||||
window.location.pathname.startsWith(linkAttr) &&
|
||||
!isLinkStrict
|
||||
twuiLinkMatchesPath({
|
||||
href: linkAttr,
|
||||
pathname: window.location.pathname,
|
||||
strict: Boolean(isLinkStrict),
|
||||
})
|
||||
) {
|
||||
linkEl.classList.add("active");
|
||||
}
|
||||
|
||||
@@ -57,6 +57,9 @@ export default function LinkList({
|
||||
...props
|
||||
}: TWUI_LINK_LIST_PROPS) {
|
||||
const listRef = React.useRef<HTMLDivElement>(null);
|
||||
const finalLinks = links
|
||||
.flat()
|
||||
.filter((ln): ln is TWUI_LINK_LIST_LINK_OBJECT => Boolean(ln));
|
||||
|
||||
React.useEffect(() => {
|
||||
twuiAddActiveLinksFn({
|
||||
@@ -75,12 +78,7 @@ export default function LinkList({
|
||||
props.className,
|
||||
)}
|
||||
>
|
||||
{links
|
||||
.flat()
|
||||
.filter((ln) => Boolean(ln))
|
||||
.map((link, index) => {
|
||||
if (!link) return null;
|
||||
|
||||
{finalLinks.map((link, index) => {
|
||||
if (link.divider)
|
||||
return (
|
||||
<React.Fragment key={index}>
|
||||
@@ -89,7 +87,7 @@ export default function LinkList({
|
||||
);
|
||||
|
||||
const finalDivider =
|
||||
index < links.length - 1 &&
|
||||
index < finalLinks.length - 1 &&
|
||||
(dividerComponent ? (
|
||||
dividerComponent
|
||||
) : divider ? (
|
||||
@@ -107,9 +105,12 @@ export default function LinkList({
|
||||
className={twMerge(
|
||||
"p-2 cursor-pointer whitespace-nowrap",
|
||||
linkProps?.className,
|
||||
buttonProps?.className,
|
||||
link.buttonProps?.className,
|
||||
)}
|
||||
onClick={(e) => {
|
||||
link.onClick?.(e);
|
||||
buttonProps?.onClick?.(e);
|
||||
link.buttonProps?.onClick?.(e);
|
||||
}}
|
||||
>
|
||||
@@ -138,6 +139,7 @@ export default function LinkList({
|
||||
strict={link.strict}
|
||||
onClick={(e) => {
|
||||
link.onClick?.(e);
|
||||
linkProps?.onClick?.(e);
|
||||
link.linkProps?.onClick?.(e);
|
||||
}}
|
||||
>
|
||||
|
||||
@@ -83,16 +83,20 @@ export default function Modal(props: TWUI_MODAL_PROPS) {
|
||||
}
|
||||
}, []);
|
||||
|
||||
const wasOpenRef = React.useRef(open);
|
||||
|
||||
React.useEffect(() => {
|
||||
existingSetOpen?.(open);
|
||||
if (open == false) onClose?.();
|
||||
if (wasOpenRef.current && !open) onClose?.();
|
||||
wasOpenRef.current = open;
|
||||
}, [open]);
|
||||
|
||||
React.useEffect(() => {
|
||||
setOpen(existingOpen || false);
|
||||
}, [existingOpen]);
|
||||
|
||||
const finalTargetRef = targetRef || React.useRef<HTMLDivElement>(null);
|
||||
const internalTargetRef = React.useRef<HTMLDivElement>(null);
|
||||
const finalTargetRef = targetRef || internalTargetRef;
|
||||
const finalPopoverReferenceRef = popoverReferenceRef || finalTargetRef;
|
||||
|
||||
const popoverTargetActiveRef = React.useRef(false);
|
||||
|
||||
@@ -26,10 +26,10 @@ export default function Pagination({
|
||||
limit,
|
||||
...props
|
||||
}: Props) {
|
||||
if (!count || !page || !limit)
|
||||
return (
|
||||
<EmptyContent title={`count, page, and limit are all required`} />
|
||||
);
|
||||
if (!page || !limit)
|
||||
return <EmptyContent title={`page and limit are both required`} />;
|
||||
|
||||
if (!count) return null;
|
||||
|
||||
const isLimit = limit * page >= count;
|
||||
|
||||
@@ -44,10 +44,8 @@ export default function Pagination({
|
||||
}}
|
||||
variant="outlined"
|
||||
size="small"
|
||||
className={twMerge(
|
||||
"p-1",
|
||||
isLimit ? "opacity-40 pointer-events-none" : "",
|
||||
)}
|
||||
className="p-1"
|
||||
disabled={isLimit}
|
||||
>
|
||||
<ChevronRight size={20} />
|
||||
</Button>
|
||||
@@ -63,17 +61,15 @@ export default function Pagination({
|
||||
>
|
||||
{pages > 1 && (
|
||||
<Button
|
||||
title="Next Page Button"
|
||||
title="Previous Page Button"
|
||||
onClick={() => {
|
||||
window.scrollTo({ top: 0, behavior: "smooth" });
|
||||
setPage?.((prev) => prev - 1);
|
||||
}}
|
||||
variant="outlined"
|
||||
size="small"
|
||||
className={twMerge(
|
||||
"p-1",
|
||||
page == 1 ? "opacity-40 pointer-events-none" : "",
|
||||
)}
|
||||
className="p-1"
|
||||
disabled={page == 1}
|
||||
>
|
||||
<ChevronLeft size={20} />
|
||||
</Button>
|
||||
|
||||
@@ -1,49 +1,57 @@
|
||||
import React from "react";
|
||||
import { serialize } from "next-mdx-remote/serialize";
|
||||
import remarkGfm from "remark-gfm";
|
||||
import rehypePrismPlus from "rehype-prism-plus";
|
||||
import { MDXRemote, type MDXRemoteSerializeResult } from "next-mdx-remote";
|
||||
import { useMDXComponents } from "../mdx/mdx-components";
|
||||
|
||||
export const TWUIPrismLanguages = ["shell", "javascript"] as const;
|
||||
|
||||
type Props = {
|
||||
content: string;
|
||||
refresh?: number;
|
||||
};
|
||||
|
||||
/**
|
||||
* # CodeBlock
|
||||
*
|
||||
* @className `twui-remote-code-block-wrapper`
|
||||
*/
|
||||
export default function RemoteCodeBlock({ content, refresh }: Props) {
|
||||
const [mdxSource, setMdxSource] =
|
||||
React.useState<MDXRemoteSerializeResult<any>>();
|
||||
|
||||
const { components } = useMDXComponents();
|
||||
|
||||
React.useEffect(() => {
|
||||
serialize(content, {
|
||||
mdxOptions: {
|
||||
remarkPlugins: [remarkGfm],
|
||||
rehypePlugins: [rehypePrismPlus],
|
||||
},
|
||||
}).then((mdxSrc) => {
|
||||
setMdxSource(mdxSrc);
|
||||
});
|
||||
}, [refresh]);
|
||||
|
||||
if (!mdxSource) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<MDXRemote
|
||||
{...mdxSource}
|
||||
components={{
|
||||
...components,
|
||||
}}
|
||||
/>
|
||||
);
|
||||
}
|
||||
import React from "react";
|
||||
import { serialize } from "next-mdx-remote/serialize";
|
||||
import remarkGfm from "remark-gfm";
|
||||
import rehypePrismPlus from "rehype-prism-plus";
|
||||
import { MDXRemote, type MDXRemoteSerializeResult } from "next-mdx-remote";
|
||||
import { useMDXComponents } from "../mdx/mdx-components";
|
||||
|
||||
export const TWUIPrismLanguages = ["shell", "javascript"] as const;
|
||||
|
||||
type Props = {
|
||||
content: string;
|
||||
refresh?: number;
|
||||
};
|
||||
|
||||
/**
|
||||
* # CodeBlock
|
||||
*
|
||||
* @className `twui-remote-code-block-wrapper`
|
||||
*/
|
||||
export default function RemoteCodeBlock({ content, refresh }: Props) {
|
||||
const [mdxSource, setMdxSource] =
|
||||
React.useState<MDXRemoteSerializeResult<any>>();
|
||||
|
||||
const { components } = useMDXComponents();
|
||||
|
||||
React.useEffect(() => {
|
||||
let cancelled = false;
|
||||
|
||||
serialize(content, {
|
||||
mdxOptions: {
|
||||
remarkPlugins: [remarkGfm],
|
||||
rehypePlugins: [rehypePrismPlus],
|
||||
},
|
||||
})
|
||||
.then((mdxSrc) => {
|
||||
if (!cancelled) setMdxSource(mdxSrc);
|
||||
})
|
||||
.catch(() => {});
|
||||
|
||||
return () => {
|
||||
cancelled = true;
|
||||
};
|
||||
}, [content, refresh]);
|
||||
|
||||
if (!mdxSource) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<MDXRemote
|
||||
{...mdxSource}
|
||||
components={{
|
||||
...components,
|
||||
}}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -9,8 +9,6 @@ import React, {
|
||||
type ReactNode,
|
||||
} from "react";
|
||||
|
||||
let timeout: any;
|
||||
|
||||
export type SearchProps<KeyType extends string> = DetailedHTMLProps<
|
||||
React.HTMLAttributes<HTMLDivElement>,
|
||||
HTMLDivElement
|
||||
@@ -50,16 +48,21 @@ export default function Search<KeyType extends string>({
|
||||
props.defaultValue?.toString() || "",
|
||||
);
|
||||
|
||||
React.useEffect(() => {
|
||||
clearTimeout(timeout);
|
||||
const timeoutRef = React.useRef<any>(null);
|
||||
|
||||
timeout = setTimeout(() => {
|
||||
React.useEffect(() => {
|
||||
window.clearTimeout(timeoutRef.current);
|
||||
|
||||
timeoutRef.current = setTimeout(() => {
|
||||
dispatch?.(input);
|
||||
changeHandler?.(input);
|
||||
}, delay);
|
||||
|
||||
return () => window.clearTimeout(timeoutRef.current);
|
||||
}, [input]);
|
||||
|
||||
const inputRef = componentRef || React.useRef<HTMLInputElement>(null);
|
||||
const internalInputRef = React.useRef<HTMLInputElement>(null);
|
||||
const inputRef = componentRef || internalInputRef;
|
||||
|
||||
// React.useEffect(() => {
|
||||
// if (props.autoFocus) {
|
||||
|
||||
@@ -18,8 +18,6 @@ export type TWUI_STAR_RATING_PROPS = DetailedHTMLProps<
|
||||
> &
|
||||
StarProps;
|
||||
|
||||
let timeout: any;
|
||||
|
||||
/**
|
||||
* # Star Rating Component
|
||||
* @className_wrapper twui-star-rating
|
||||
@@ -41,12 +39,15 @@ export default function StarRating({
|
||||
|
||||
const starClicked = React.useRef(false);
|
||||
const sectionHovered = React.useRef(false);
|
||||
const timeoutRef = React.useRef<any>(null);
|
||||
|
||||
React.useEffect(() => {
|
||||
window.clearTimeout(timeout);
|
||||
timeout = setTimeout(() => {
|
||||
setValueExternal?.(finalValue);
|
||||
window.clearTimeout(timeoutRef.current);
|
||||
timeoutRef.current = setTimeout(() => {
|
||||
setValueExternal?.(selectedStarValue);
|
||||
}, 500);
|
||||
|
||||
return () => window.clearTimeout(timeoutRef.current);
|
||||
}, [selectedStarValue]);
|
||||
|
||||
return (
|
||||
@@ -148,6 +149,7 @@ function StarComponent({
|
||||
>
|
||||
<Star
|
||||
size={size}
|
||||
{...starProps}
|
||||
className={twMerge(
|
||||
"text-slate-300 dark:text-white/20",
|
||||
isActive &&
|
||||
@@ -156,7 +158,6 @@ function StarComponent({
|
||||
// "hover:text-orange-500 hover:dark:text-orange-400 hover:fill-orange-500 hover:dark:fill-orange-400",
|
||||
starProps?.className,
|
||||
)}
|
||||
{...starProps}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
|
||||
+14
-2
@@ -6,6 +6,18 @@ type Props = {
|
||||
data?: { [k: string]: any }[];
|
||||
};
|
||||
|
||||
function formatCellValue(value: unknown): string {
|
||||
if (value === null || value === undefined) return "";
|
||||
if (typeof value === "object") {
|
||||
try {
|
||||
return JSON.stringify(value);
|
||||
} catch {
|
||||
return String(value);
|
||||
}
|
||||
}
|
||||
return String(value);
|
||||
}
|
||||
|
||||
export default function Table({ data }: Props) {
|
||||
if (!data || data.length === 0) {
|
||||
return (
|
||||
@@ -62,9 +74,9 @@ export default function Table({ data }: Props) {
|
||||
"dark:text-foreground-dark max-w-[200px] overflow-hidden",
|
||||
"overflow-ellipsis",
|
||||
)}
|
||||
title={row[header]}
|
||||
title={formatCellValue(row[header])}
|
||||
>
|
||||
{row[header]}
|
||||
{formatCellValue(row[header])}
|
||||
</td>
|
||||
))}
|
||||
</tr>
|
||||
|
||||
@@ -19,9 +19,6 @@ export type TWUIToastProps = DetailedHTMLProps<
|
||||
color?: (typeof ToastStyles)[number];
|
||||
};
|
||||
|
||||
let interval: any;
|
||||
let timeout: any;
|
||||
|
||||
/**
|
||||
* # Toast Component
|
||||
* @className twui-toast-root
|
||||
@@ -38,6 +35,7 @@ export default function Toast({
|
||||
...props
|
||||
}: TWUIToastProps) {
|
||||
const [ready, setReady] = React.useState(false);
|
||||
const timeoutRef = React.useRef<any>(null);
|
||||
const IDName = "twui-toast-root";
|
||||
|
||||
React.useEffect(() => {
|
||||
@@ -56,14 +54,13 @@ export default function Toast({
|
||||
React.useEffect(() => {
|
||||
if (!ready || !open) return;
|
||||
|
||||
timeout = setTimeout(() => {
|
||||
timeoutRef.current = setTimeout(() => {
|
||||
setOpen?.(false);
|
||||
closeDispatch?.(open);
|
||||
}, closeDelay);
|
||||
|
||||
return function () {
|
||||
setOpen?.(false);
|
||||
closeDispatch?.(open);
|
||||
window.clearTimeout(timeoutRef.current);
|
||||
};
|
||||
}, [ready, open]);
|
||||
|
||||
@@ -85,10 +82,11 @@ export default function Toast({
|
||||
"twui-toast",
|
||||
)}
|
||||
onMouseEnter={() => {
|
||||
window.clearTimeout(timeout);
|
||||
window.clearTimeout(timeoutRef.current);
|
||||
}}
|
||||
onMouseLeave={(e) => {
|
||||
timeout = setTimeout(() => {
|
||||
window.clearTimeout(timeoutRef.current);
|
||||
timeoutRef.current = setTimeout(() => {
|
||||
setOpen?.(false);
|
||||
closeDispatch?.(open);
|
||||
}, closeDelay);
|
||||
|
||||
@@ -90,6 +90,7 @@ export default function AIPromptActionSection({
|
||||
loading={loading}
|
||||
className="p-2"
|
||||
onClick={() => {
|
||||
if (!prompt.trim()) return;
|
||||
currentPromptRef.current = prompt;
|
||||
setTimeout(() => {
|
||||
setPrompt("");
|
||||
|
||||
@@ -28,9 +28,7 @@ export default function AIPromptBlock({
|
||||
setMdRes,
|
||||
placeholder,
|
||||
}: Props) {
|
||||
const [prompt, setPrompt] = React.useState("");
|
||||
const currentPromptRef = React.useRef("");
|
||||
const promptInputRef = React.useRef<HTMLTextAreaElement>(null);
|
||||
|
||||
return (
|
||||
<Paper className="">
|
||||
@@ -64,6 +62,7 @@ export default function AIPromptBlock({
|
||||
mdRes,
|
||||
setMdRes,
|
||||
placeholder,
|
||||
currentPromptRef,
|
||||
}}
|
||||
/>
|
||||
</Stack>
|
||||
|
||||
@@ -13,6 +13,8 @@ type Props = {
|
||||
mdRes?: string;
|
||||
setMdRes: React.Dispatch<React.SetStateAction<string>>;
|
||||
placeholder?: string;
|
||||
/** Receives the last submitted prompt */
|
||||
currentPromptRef?: React.MutableRefObject<string>;
|
||||
};
|
||||
|
||||
export default function AIPromptBlockInputSection({
|
||||
@@ -23,9 +25,12 @@ export default function AIPromptBlockInputSection({
|
||||
mdRes = "",
|
||||
setMdRes,
|
||||
placeholder,
|
||||
currentPromptRef: externalCurrentPromptRef,
|
||||
}: Props) {
|
||||
const [prompt, setPrompt] = React.useState("");
|
||||
const currentPromptRef = React.useRef("");
|
||||
const internalCurrentPromptRef = React.useRef("");
|
||||
const currentPromptRef =
|
||||
externalCurrentPromptRef || internalCurrentPromptRef;
|
||||
const promptInputRef = React.useRef<HTMLTextAreaElement>(null);
|
||||
|
||||
return (
|
||||
@@ -44,6 +49,7 @@ export default function AIPromptBlockInputSection({
|
||||
onKeyDown={(e) => {
|
||||
if (e.key == "Enter" && !e.ctrlKey && !e.shiftKey) {
|
||||
e.preventDefault();
|
||||
if (!prompt.trim() || loading) return;
|
||||
currentPromptRef.current = prompt;
|
||||
setTimeout(() => {
|
||||
setPrompt("");
|
||||
|
||||
@@ -53,9 +53,10 @@ export default function AIVoiceTranscription({
|
||||
onClick={(e) => {
|
||||
e.preventDefault();
|
||||
|
||||
const history_last_response = history?.find(
|
||||
(h) => h.role == "assistant",
|
||||
);
|
||||
const history_last_response = history
|
||||
?.slice()
|
||||
.reverse()
|
||||
.find((h) => h.role == "assistant");
|
||||
voice_transcription_fn({
|
||||
last_message:
|
||||
last_response ||
|
||||
|
||||
Reference in New Issue
Block a user