This commit is contained in:
2025-12-02 16:30:46 +01:00
parent 979728e6c8
commit 0b7c70058d
31 changed files with 298 additions and 106 deletions
+4
View File
@@ -9,6 +9,8 @@ export const TWUIDropdownContentPositions = [
"left",
"bottom-left",
"top-left",
"top",
"bottom",
"right",
"bottom-right",
"top-right",
@@ -160,6 +162,8 @@ export default function Dropdown({
? "right-0 top-[100%]"
: position == "center"
? "left-[50%] -translate-x-[50%] top-[100%]"
: position == "top"
? "left-[50%] -translate-x-[50%] bottom-[100%]"
: "top-[100%]",
above ? "-translate-y-[120%]" : "",
open ? "flex" : "hidden",
+12 -2
View File
@@ -2,19 +2,26 @@ import { ComponentProps, DetailedHTMLProps, HTMLAttributes } from "react";
import { twMerge } from "tailwind-merge";
import Center from "../layout/Center";
import Loading from "./Loading";
import Row from "../layout/Row";
import Span from "../layout/Span";
type Props = DetailedHTMLProps<
HTMLAttributes<HTMLDivElement>,
HTMLDivElement
> & {
loadingProps?: ComponentProps<typeof Loading>;
label?: string;
};
/**
* # Loading Overlay Component
* @className_wrapper twui-loading-overlay
*/
export default function LoadingOverlay({ loadingProps, ...props }: Props) {
export default function LoadingOverlay({
loadingProps,
label,
...props
}: Props) {
return (
<div
{...props}
@@ -26,7 +33,10 @@ export default function LoadingOverlay({ loadingProps, ...props }: Props) {
)}
>
<Center>
<Loading {...loadingProps} />
<Row>
<Loading {...loadingProps} />
{label && <Span>{label}</Span>}
</Row>
</Center>
</div>
);
+7 -2
View File
@@ -32,6 +32,7 @@ export type TWUI_MODAL_PROPS = DetailedHTMLProps<
trigger?: (typeof TWUIPopoverTriggers)[number];
debounce?: number;
onClose?: () => any;
hoverOpen?: boolean;
};
/**
@@ -55,6 +56,7 @@ export default function Modal(props: TWUI_MODAL_PROPS) {
trigger = "hover",
debounce = 500,
onClose,
hoverOpen,
} = props;
const [ready, setReady] = React.useState(false);
@@ -65,6 +67,9 @@ export default function Modal(props: TWUI_MODAL_PROPS) {
const modalRoot = document.getElementById(IDName);
if (modalRoot) {
if (isPopover) {
modalRoot.style.zIndex = "1000";
}
setReady(true);
} else {
const newModalRootEl = document.createElement("div");
@@ -144,12 +149,12 @@ export default function Modal(props: TWUI_MODAL_PROPS) {
onClick={(e) => setOpen(!open)}
ref={finalTargetRef}
onMouseEnter={
isPopover && trigger === "hover"
isPopover && (trigger === "hover" || hoverOpen)
? popoverEnterFn
: targetWrapperProps?.onMouseEnter
}
onMouseLeave={
isPopover && trigger === "hover"
isPopover && (trigger === "hover" || hoverOpen)
? popoverLeaveFn
: targetWrapperProps?.onMouseLeave
}
+4 -2
View File
@@ -21,6 +21,7 @@ export type SearchProps<KeyType extends string> = DetailedHTMLProps<
>;
loading?: boolean;
placeholder?: string;
componentRef?: React.RefObject<HTMLInputElement | null>;
};
/**
@@ -37,6 +38,7 @@ export default function Search<KeyType extends string>({
buttonProps,
loading,
placeholder,
componentRef,
...props
}: SearchProps<KeyType>) {
const [input, setInput] = React.useState(
@@ -52,7 +54,7 @@ export default function Search<KeyType extends string>({
}, delay);
}, [input]);
const inputRef = React.useRef<HTMLInputElement>(null);
const inputRef = componentRef || React.useRef<HTMLInputElement>(null);
React.useEffect(() => {
if (props.autoFocus) {
@@ -64,7 +66,7 @@ export default function Search<KeyType extends string>({
<Row
{...props}
className={twMerge(
"relative xl:flex-nowrap items-stretch gap-0",
"relative xl:flex-nowrap items-stretch gap-0 flex-nowrap",
"twui-search-wrapper",
props?.className
)}
+2 -2
View File
@@ -31,7 +31,7 @@ export default function Table({ data }: Props) {
<th
key={header}
className={twMerge(
"px-3 py-2 text-left text-sm opacity-50",
"px-3 py-2 text-left opacity-50",
"font-semibold"
)}
title={header}
@@ -58,7 +58,7 @@ export default function Table({ data }: Props) {
<td
key={`${header}-${index}`}
className={twMerge(
"px-3 py-2 whitespace-nowrap text-sm text-foreground-light",
"px-3 py-2 whitespace-nowrap text-foreground-light",
"dark:text-foreground-dark max-w-[200px] overflow-hidden",
"overflow-ellipsis"
)}
+6 -3
View File
@@ -8,7 +8,7 @@ import twuiSlugify from "../utils/slugify";
export type TWUITabsObject = {
title: string;
value?: string;
content: React.ReactNode;
content?: React.ReactNode;
defaultActive?: boolean;
};
@@ -35,6 +35,8 @@ export type TWUI_TOGGLE_PROPS = React.ComponentProps<typeof Stack> & {
* @className twui-tab-buttons
* @className twui-tab-button-active
* @className twui-tab-buttons-wrapper
* @className twui-tab-buttons-container
* @className twui-tabs-border
*/
export default function Tabs({
tabsContentArray,
@@ -91,13 +93,14 @@ export default function Tabs({
)}
>
<Border
className="p-0 w-full overflow-hidden"
className="p-0 w-full overflow-hidden twui-tabs-border"
{...tabsBorderProps}
>
<Row
className={twMerge(
"gap-0 items-stretch w-full flex-nowrap overflow-x-auto",
centered && "justify-center"
centered && "justify-center",
"twui-tab-buttons-container"
)}
>
{values.map((value, index) => {
+9 -3
View File
@@ -18,6 +18,7 @@ type Props = {
loading?: boolean;
mdRes?: string;
setMdRes: React.Dispatch<React.SetStateAction<string>>;
placeholder?: string;
};
export default function AIPromptBlock({
@@ -27,6 +28,7 @@ export default function AIPromptBlock({
loading = false,
mdRes = "",
setMdRes,
placeholder,
}: Props) {
const [prompt, setPrompt] = React.useState("");
const currentPromptRef = React.useRef("");
@@ -43,7 +45,7 @@ export default function AIPromptBlock({
<MessageCircleMore
size={15}
opacity={0.5}
className="-mt-[1px]"
className="-mt-px"
/>
</Row>
</Card>
@@ -53,11 +55,15 @@ export default function AIPromptBlock({
setStreamRes={setMdRes}
streamRes={mdRes}
history={history}
loading={loading}
/>
<Stack className="w-full relative">
{loading && <LoadingOverlay />}
<Textarea
placeholder={model ? `Prompt ${model}` : "Prompt AI"}
placeholder={
placeholder ||
(model ? `Prompt ${model}` : "Prompt AI")
}
wrapperProps={{ className: "outline-none" }}
wrapperWrapperProps={{ className: "w-full" }}
value={prompt}
@@ -65,7 +71,7 @@ export default function AIPromptBlock({
setPrompt(e.target.value);
}}
onKeyDown={(e) => {
if (e.key == "Enter" && !e.ctrlKey) {
if (e.key == "Enter" && !e.ctrlKey && !e.shiftKey) {
e.preventDefault();
currentPromptRef.current = prompt;
setTimeout(() => {
@@ -8,12 +8,14 @@ type Props = {
streamRes: string;
setStreamRes: React.Dispatch<React.SetStateAction<string>>;
history: ChatCompletionMessageParam[];
loading?: boolean;
};
export default function AIPromptPreview({
setStreamRes,
streamRes,
history,
loading,
}: Props) {
const responseContentRef = React.useRef<HTMLDivElement>(null);
const isContentInterrupted = React.useRef(false);
@@ -27,7 +29,7 @@ export default function AIPromptPreview({
}
}, [streamRes]);
if (!streamRes?.match(/./)) return null;
if (loading || !streamRes?.match(/./)) return null;
return (
<Stack className="w-full">