This commit is contained in:
2026-02-13 19:04:07 +01:00
parent 9505331165
commit 69d432b6af
42 changed files with 650 additions and 360 deletions
+6 -5
View File
@@ -13,6 +13,7 @@ import Button from "../layout/Button";
export type TWUI_LINK_LIST_LINK_OBJECT = {
title?: string;
component?: ReactNode;
url?: string;
strict?: boolean;
icon?: ReactNode;
@@ -70,7 +71,7 @@ export default function LinkList({
className={twMerge(
"flex flex-row items-center gap-1",
"twui-link-list",
props.className
props.className,
)}
>
{links
@@ -104,7 +105,7 @@ export default function LinkList({
{...link.buttonProps}
className={twMerge(
"p-2 cursor-pointer whitespace-nowrap",
linkProps?.className
linkProps?.className,
)}
onClick={(e) => {
link.onClick?.(e);
@@ -113,7 +114,7 @@ export default function LinkList({
>
<Row>
{link.icon}
{link.title}
{link.component || link.title}
</Row>
</Button>
{finalDivider}
@@ -131,7 +132,7 @@ export default function LinkList({
className={twMerge(
"p-2 cursor-pointer whitespace-nowrap",
linkProps?.className,
link.linkProps?.className
link.linkProps?.className,
)}
strict={link.strict}
onClick={(e) => {
@@ -144,7 +145,7 @@ export default function LinkList({
link.iconPosition == "before"
? link.icon
: null}
{link.title}
{link.component || link.title}
{link.iconPosition == "after"
? link.icon
: null}
+6 -2
View File
@@ -31,14 +31,18 @@ export default function Loading({ size, svgClassName, ...props }: Props) {
})();
return (
<div role="status" {...props}>
<div
role="status"
{...props}
className={twMerge(`twui-loading`, props.className)}
>
<svg
aria-hidden="true"
className={twMerge(
"text-gray animate-spin dark:text-gray-dark fill-primary",
"dark:fill-white twui-loading",
sizeClassName,
svgClassName
svgClassName,
)}
viewBox="0 0 100 101"
fill="none"
+5 -2
View File
@@ -11,6 +11,7 @@ type Props = DetailedHTMLProps<
> & {
loadingProps?: ComponentProps<typeof Loading>;
label?: string;
fixed?: boolean;
};
/**
@@ -20,16 +21,18 @@ type Props = DetailedHTMLProps<
export default function LoadingOverlay({
loadingProps,
label,
fixed,
...props
}: Props) {
return (
<div
{...props}
className={twMerge(
"absolute top-0 left-0 w-full h-full z-[500]",
"top-0 left-0 w-full h-full z-[500]",
"bg-background-light/90 dark:bg-background-dark/90",
fixed ? "fixed" : "absolute",
props.className,
"twui-loading-overlay"
"twui-loading-overlay",
)}
>
<Center>
+5 -1
View File
@@ -146,7 +146,11 @@ export default function Modal(props: TWUI_MODAL_PROPS) {
{target ? (
<div
{...targetWrapperProps}
onClick={(e) => setOpen(!open)}
onClick={(e) => {
e.preventDefault();
e.stopPropagation();
setOpen(!open);
}}
ref={finalTargetRef}
onMouseEnter={
isPopover && (trigger === "hover" || hoverOpen)
+3 -2
View File
@@ -9,6 +9,7 @@ export const TWUIPrismLanguages = ["shell", "javascript"] as const;
type Props = {
content: string;
refresh?: number;
};
/**
@@ -16,7 +17,7 @@ type Props = {
*
* @className `twui-remote-code-block-wrapper`
*/
export default function RemoteCodeBlock({ content }: Props) {
export default function RemoteCodeBlock({ content, refresh }: Props) {
const [mdxSource, setMdxSource] =
React.useState<MDXRemoteSerializeResult<any>>();
@@ -31,7 +32,7 @@ export default function RemoteCodeBlock({ content }: Props) {
}).then((mdxSrc) => {
setMdxSource(mdxSrc);
});
}, []);
}, [refresh]);
if (!mdxSource) {
return null;
+3 -3
View File
@@ -78,12 +78,12 @@ export default function Search<KeyType extends string>({
value={input}
onChange={(e) => setInput(e.target.value)}
className={twMerge(
"rounded-r-none",
"rounded-r-none!",
"twui-search-input",
inputProps?.className
)}
wrapperProps={{
className: "rounded-r-none",
className: "rounded-r-none!",
}}
componentRef={inputRef}
/>
@@ -93,7 +93,7 @@ export default function Search<KeyType extends string>({
variant="outlined"
color="gray"
className={twMerge(
"rounded-l-none ml-[1px]",
"rounded-l-none! ml-[1px]",
"twui-search-button",
buttonProps?.className
)}
+12 -8
View File
@@ -14,6 +14,7 @@ type StarProps = {
starProps?: LucideProps;
allowRating?: boolean;
setValueExternal?: React.Dispatch<React.SetStateAction<number>>;
changeHandler?: (value: number) => void;
};
export type TWUI_STAR_RATING_PROPS = DetailedHTMLProps<
@@ -35,6 +36,7 @@ export default function StarRating({
starProps,
allowRating,
setValueExternal,
changeHandler,
...props
}: TWUI_STAR_RATING_PROPS) {
const totalArray = Array(total).fill(null);
@@ -58,7 +60,7 @@ export default function StarRating({
className={twMerge(
"flex flex-row items-center gap-0 -ml-[2px]",
"twui-star-rating",
props.className
props.className,
)}
onMouseEnter={() => {
sectionHovered.current = true;
@@ -68,6 +70,8 @@ export default function StarRating({
}}
>
{totalArray.map((_, index) => {
const isActive = index + 1 <= finalValue;
return (
<StarComponent
{...{
@@ -83,6 +87,8 @@ export default function StarRating({
selectedStarValue,
sectionHovered,
setSelectedStarValue,
isActive,
changeHandler,
}}
key={index}
/>
@@ -93,34 +99,31 @@ export default function StarRating({
}
function StarComponent({
value = 0,
size = 20,
starProps,
index,
allowRating,
finalValue,
setFinalValue,
starClicked,
sectionHovered,
setSelectedStarValue,
selectedStarValue,
isActive,
changeHandler,
}: StarProps & {
index: number;
finalValue: number;
setFinalValue: React.Dispatch<React.SetStateAction<number>>;
setSelectedStarValue: React.Dispatch<React.SetStateAction<number>>;
starClicked: React.MutableRefObject<boolean>;
sectionHovered: React.MutableRefObject<boolean>;
selectedStarValue: number;
isActive: boolean;
}) {
const isActive = index < finalValue;
return (
<div
className={twMerge("p-[2px]", allowRating && "cursor-pointer")}
onMouseEnter={() => {
if (!allowRating) return;
setFinalValue(index + 1);
}}
onMouseLeave={() => {
@@ -145,6 +148,7 @@ function StarComponent({
starClicked.current = true;
setSelectedStarValue(index + 1);
changeHandler?.(index + 1);
}}
>
<Star
@@ -155,7 +159,7 @@ function StarComponent({
"text-orange-500 dark:text-orange-400 fill-orange-500 dark:fill-orange-400",
// allowRating &&
// "hover:text-orange-500 hover:dark:text-orange-400 hover:fill-orange-500 hover:dark:fill-orange-400",
starProps?.className
starProps?.className,
)}
{...starProps}
/>
+43 -10
View File
@@ -27,6 +27,8 @@ export type TWUI_TOGGLE_PROPS = React.ComponentProps<typeof Stack> & {
switchComponent?: ReactNode;
setActiveValue?: React.Dispatch<React.SetStateAction<string | undefined>>;
changeHandler?: (value: TWUITabsObject) => void;
defaultValue?: string | null;
hrefUpdate?: boolean;
};
/**
@@ -47,6 +49,8 @@ export default function Tabs({
switchComponent,
setActiveValue: existingSetActiveValue,
changeHandler,
defaultValue,
hrefUpdate,
...props
}: TWUI_TOGGLE_PROPS) {
const finalTabsContentArray = tabsContentArray
@@ -54,31 +58,60 @@ export default function Tabs({
.filter((ct) => Boolean(ct?.title)) as TWUITabsObject[];
const values = finalTabsContentArray.map(
(obj) => obj.value || twuiSlugify(obj.title)
(obj) => obj.value || twuiSlugify(obj.title),
);
const defaultActiveObj = finalTabsContentArray.find(
(ctn) => ctn.defaultActive
(ctn) => ctn.defaultActive,
);
const [activeValue, setActiveValue] = React.useState(
defaultActiveObj
? defaultActiveObj?.value || twuiSlugify(defaultActiveObj.title)
: values[0] || undefined
defaultValue
? defaultValue
: defaultActiveObj
? defaultActiveObj?.value || twuiSlugify(defaultActiveObj.title)
: values[0] || undefined,
);
const [ready, setReady] = React.useState(false);
const targetContent = finalTabsContentArray.find(
(ctn) =>
ctn.value == activeValue || twuiSlugify(ctn.title) == activeValue
ctn.value == activeValue || twuiSlugify(ctn.title) == activeValue,
);
React.useEffect(() => {
if (!ready) return;
existingSetActiveValue?.(activeValue);
if (targetContent && activeValue) {
changeHandler?.(targetContent);
if (hrefUpdate) {
const url = new URL(window.location.href);
url.searchParams.set("tab", activeValue);
window.history.pushState({}, "", url);
}
}
}, [activeValue]);
React.useEffect(() => {
if (hrefUpdate) {
const url = new URL(window.location.href);
const activeTab = url.searchParams.get("tab");
if (activeTab && activeValue !== activeTab) {
setActiveValue(undefined);
setActiveValue(activeTab);
}
setTimeout(() => {
setReady(true);
}, 500);
} else {
setReady(true);
}
}, []);
return (
<Stack
{...props}
@@ -89,7 +122,7 @@ export default function Tabs({
className={twMerge(
"w-full",
"twui-tab-buttons-wrapper",
tabsButtonsWrapperProps?.className
tabsButtonsWrapperProps?.className,
)}
>
<Border
@@ -100,14 +133,14 @@ export default function Tabs({
className={twMerge(
"gap-0 items-stretch w-full flex-nowrap overflow-x-auto",
centered && "justify-center",
"twui-tab-buttons-container"
"twui-tab-buttons-container",
)}
>
{values.map((value, index) => {
const targetObject = finalTabsContentArray.find(
(ctn) =>
ctn.value == value ||
twuiSlugify(ctn.title) == value
twuiSlugify(ctn.title) == value,
);
const isActive = value == activeValue;
@@ -120,7 +153,7 @@ export default function Tabs({
? "bg-primary dark:bg-primary-dark text-white outline-none twui-tab-button-active"
: "text-slate-400 dark:text-white/40 hover:text-slate-800 dark:hover:text-white" +
" cursor-pointer",
"twui-tab-buttons"
"twui-tab-buttons",
)}
onClick={() => {
setActiveValue(undefined);
+17 -7
View File
@@ -14,6 +14,7 @@ export type TWUIToastProps = DetailedHTMLProps<
> & {
open?: boolean;
setOpen?: React.Dispatch<React.SetStateAction<boolean>>;
closeDispatch?: (open?: boolean) => void;
closeDelay?: number;
color?: (typeof ToastStyles)[number];
};
@@ -33,6 +34,7 @@ export default function Toast({
setOpen,
closeDelay = 4000,
color,
closeDispatch,
...props
}: TWUIToastProps) {
const [ready, setReady] = React.useState(false);
@@ -56,10 +58,12 @@ export default function Toast({
timeout = setTimeout(() => {
setOpen?.(false);
closeDispatch?.(open);
}, closeDelay);
return function () {
setOpen?.(false);
closeDispatch?.(open);
};
}, [ready, open]);
@@ -73,12 +77,12 @@ export default function Toast({
"fixed bottom-4 right-4 z-[250] border-none",
"pl-6 pr-8 py-4 bg-primary dark:bg-primary-dark",
color == "success"
? "bg-success dark:bg-success-dark twui-toast-success"
? "bg-success-dark dark:bg-success-dark twui-toast-success"
: color == "error"
? "bg-error dark:bg-error-dark twui-toast-error"
: "",
? "bg-error dark:bg-error-dark twui-toast-error"
: "",
props.className,
"twui-toast"
"twui-toast",
)}
onMouseEnter={() => {
window.clearTimeout(timeout);
@@ -86,22 +90,28 @@ export default function Toast({
onMouseLeave={(e) => {
timeout = setTimeout(() => {
setOpen?.(false);
closeDispatch?.(open);
}, closeDelay);
}}
>
<Span
className={twMerge(
"absolute top-2 right-2 z-[100] cursor-pointer",
"text-white"
"text-white",
)}
onClick={(e) => {
e.preventDefault();
e.stopPropagation();
setOpen?.(false);
closeDispatch?.(open);
}}
>
<X size={15} />
</Span>
<Span className={twMerge("text-white")}>{props.children}</Span>
<Span className={twMerge("text-white! font-semibold")}>
{props.children}
</Span>
</Card>,
document.getElementById(IDName) as HTMLElement
document.getElementById(IDName) as HTMLElement,
);
}
@@ -32,7 +32,7 @@ export default function AIPromptHistoryModal({ history }: Props) {
View History
</Button>
}
className="max-w-[900px] bg-slate-100 dark:bg-white/5 xl:p-10"
className="max-w-[900px] bg-slate-100 dark:bg-white/5 xl:p-8"
>
<Stack className="gap-10 w-full">
<Stack className="gap-1">
@@ -1,7 +1,7 @@
import Divider from "@/src/components/twui/layout/Divider";
import Stack from "@/src/components/twui/layout/Stack";
import Divider from "../../layout/Divider";
import Stack from "../../layout/Stack";
import React from "react";
import MarkdownEditorPreviewComponent from "@/src/components/twui/mdx/markdown/MarkdownEditorPreviewComponent";
import MarkdownEditorPreviewComponent from "../../mdx/markdown/MarkdownEditorPreviewComponent";
import { ChatCompletionMessageParam } from "openai/resources/index";
type Props = {
+20
View File
@@ -0,0 +1,20 @@
import type { LucideProps } from "lucide-react";
import * as icons from "lucide-react";
import React from "react";
export type TWUILucideIconName = keyof typeof icons;
export type TWUILucideIconProps = LucideProps & {
name: TWUILucideIconName;
};
export default function LucideIcon({ name, ...props }: TWUILucideIconProps) {
const IconComponent = icons[name] as any;
if (!IconComponent) {
console.warn(`Lucide icon "${name}" not found`);
return null;
}
return <IconComponent {...props} />;
}