This commit is contained in:
2026-10-01 08:02:47 +01:00
parent 6392457ffb
commit 3dc9f12ad4
104 changed files with 6536 additions and 1037 deletions
+4 -4
View File
@@ -1,4 +1,4 @@
import { DetailedHTMLProps, HTMLAttributes, RefObject } from "react";
import type { DetailedHTMLProps, HTMLAttributes, RefObject } from "react";
import { twMerge } from "tailwind-merge";
export type TWUI_BORDER_PROPS = DetailedHTMLProps<
@@ -28,11 +28,11 @@ export default function Border({
? spacing == "normal"
? "px-3 py-2"
: spacing == "tight"
? "px-2 py-1"
: ""
? "px-2 py-1"
: ""
: "px-3 py-2",
"twui-border",
props.className
props.className,
)}
ref={componentRef}
>
+10 -10
View File
@@ -1,4 +1,4 @@
import React, { ComponentProps, ReactNode } from "react";
import React, { type ComponentProps, type ReactNode } from "react";
import Link from "../layout/Link";
import Divider from "../layout/Divider";
import Row from "../layout/Row";
@@ -47,7 +47,7 @@ export default function Breadcrumbs({
const [links, setLinks] = React.useState<LinkObject[] | null>(
pageUrl
? twuiBreadcrumbsGenerateLinksFromUrl({ url: pageUrl, skipHome })
: null
: null,
);
React.useEffect(() => {
@@ -76,13 +76,13 @@ export default function Breadcrumbs({
<nav
className={twMerge(
"overflow-x-auto",
"twui-current-breadcrumb-wrapper"
"twui-current-breadcrumb-wrapper",
)}
aria-label="Breadcrumb"
>
<Row
className={twMerge(
"gap-4 flex-nowrap whitespace-nowrap overflow-x-auto overflow-y-hidden w-full"
"gap-4 flex-nowrap whitespace-nowrap overflow-x-auto overflow-y-hidden w-full",
)}
>
{backButton && (
@@ -94,7 +94,7 @@ export default function Breadcrumbs({
className={twMerge(
"p-1 -my-2 -mx-2",
"twui-breadcrumbs-back-button",
backButtonProps?.className
backButtonProps?.className,
)}
onClick={(e) => {
window.history.back();
@@ -108,7 +108,7 @@ export default function Breadcrumbs({
vertical
className={twMerge(
"twui-breadcrumbs-divider",
dividerProps?.className
dividerProps?.className,
)}
/>
)}
@@ -131,7 +131,7 @@ export default function Breadcrumbs({
isTarget ? "current" : "",
"twui-breadcrumb-link",
linkProps?.className,
isTarget && currentLinkProps?.className
isTarget && currentLinkProps?.className,
)}
title={
currentLinkProps?.title || linkObject.title
@@ -152,7 +152,7 @@ export default function Breadcrumbs({
isTarget ? "current" : "",
"twui-breadcrumb-link",
linkProps?.className,
isTarget && currentLinkProps?.className
isTarget && currentLinkProps?.className,
)}
>
{currentLinkProps?.title ||
@@ -164,7 +164,7 @@ export default function Breadcrumbs({
{...dividerProps}
className={twMerge(
"twui-breadcrumbs-divider",
dividerProps?.className
dividerProps?.className,
)}
/>
)}
@@ -214,7 +214,7 @@ export function twuiBreadcrumbsGenerateLinksFromUrl({
for (let i = 0; i < array.length; i++) {
const lnText = array[i];
if (i > index || !lnText.match(/./)) continue;
if (i > index || !lnText?.match(/./)) continue;
path += `/${lnText}`;
}
+5 -5
View File
@@ -1,7 +1,7 @@
import React, {
ComponentProps,
DetailedHTMLProps,
HTMLAttributes,
type ComponentProps,
type DetailedHTMLProps,
type HTMLAttributes,
} from "react";
import { twMerge } from "tailwind-merge";
import Link from "../layout/Link";
@@ -43,7 +43,7 @@ export default function Card({
"flex flex-row items-center p-4 rounded-default bg-background-light dark:bg-background-dark",
"border border-slate-200 dark:border-white/10 border-solid",
noHover ? "" : "twui-card",
props.className
props.className,
)}
>
{props.children}
@@ -60,7 +60,7 @@ export default function Card({
"cursor-pointer",
"twui-card",
"twui-card-link",
linkProps?.className
linkProps?.className,
)}
>
{component}
+3 -3
View File
@@ -1,5 +1,5 @@
import { ComponentProps, ReactNode } from "react";
import Stack from "../layout/Stack copy";
import type { ComponentProps, ReactNode } from "react";
import Stack from "../layout/Stack";
import Row from "../layout/Row";
import { Check, CheckCircle, CheckCircle2 } from "lucide-react";
import Span from "../layout/Span";
@@ -37,7 +37,7 @@ export default function CheckBulletPoints({
{...bulletWrapperProps}
className={twMerge(
"gap-2 xl:flex-nowrap",
bulletWrapperProps?.className
bulletWrapperProps?.className,
)}
>
{bulletPoint.icon || (
+8 -8
View File
@@ -1,8 +1,8 @@
import { Check, Copy } from "lucide-react";
import React, {
DetailedHTMLProps,
HTMLAttributes,
PropsWithChildren,
type DetailedHTMLProps,
type HTMLAttributes,
type PropsWithChildren,
} from "react";
import { twMerge } from "tailwind-merge";
import Stack from "../layout/Stack";
@@ -56,7 +56,7 @@ export default function CodeBlock({
`rounded w-full transition-all items-start`,
"relative max-w-[80vw] sm:max-w-[85vw] xl:max-w-[880px]",
"twui-code-block-wrapper",
wrapperProps?.className
wrapperProps?.className,
)}
style={{
boxShadow: copied
@@ -69,14 +69,14 @@ export default function CodeBlock({
<Stack
className={twMerge(
"gap-0 w-full overflow-x-auto relative",
"max-h-[600px] overflow-y-auto"
"max-h-[600px] overflow-y-auto",
)}
>
<Row
className={twMerge(
"w-full px-1 h-10 sticky top-0 py-2",
singleBlock ? "absolute !bg-transparent" : "",
"twui-code-block-header"
"twui-code-block-header",
)}
style={{
backgroundColor: finalBackgroundColor,
@@ -128,7 +128,7 @@ export default function CodeBlock({
className={twMerge(
`p-1 w-full [&_pre]:!bg-transparent`,
singleBlock ? "" : "-mt-1",
"twui-code-pre-wrapper"
"twui-code-pre-wrapper",
)}
ref={codeRef as any}
>
@@ -138,7 +138,7 @@ export default function CodeBlock({
"!my-0 whitespace-pre-wrap",
language ? `language-${language}` : "",
"twui-code-block-pre",
props.className
props.className,
)}
>
{children}
+4 -4
View File
@@ -1,4 +1,4 @@
import React, { DetailedHTMLProps, HTMLAttributes } from "react";
import React, { type DetailedHTMLProps, type HTMLAttributes } from "react";
import { twMerge } from "tailwind-merge";
import { Moon, Sun } from "lucide-react";
@@ -75,21 +75,21 @@ export default function ColorSchemeSelector({
className={twMerge(
"flex flex-row items-center",
"twui-color-scheme-selector",
props.className
props.className,
)}
>
<button
title="Color Scheme Selector Button"
onClick={() => setActive(!active)}
className={twMerge(
"cursor-pointer hover:opacity-70 flex items-center justify-center"
"cursor-pointer hover:opacity-70 flex items-center justify-center",
)}
>
<div
{...iconWrapperProps}
className={twMerge(
"w-6 h-6 flex items-center justify-center",
iconWrapperProps?.className
iconWrapperProps?.className,
)}
>
{active == false && <Sun />}
+2 -2
View File
@@ -1,10 +1,10 @@
import React, {
import type {
ComponentProps,
Dispatch,
ReactNode,
SetStateAction,
} from "react";
import { Copy, LucideProps } from "lucide-react";
import { Copy, type LucideProps } from "lucide-react";
import Button from "../layout/Button";
type Props = Omit<ComponentProps<typeof Button>, "title"> & {
+16 -16
View File
@@ -1,7 +1,7 @@
import React, {
DetailedHTMLProps,
HTMLAttributes,
PropsWithChildren,
type DetailedHTMLProps,
type HTMLAttributes,
type PropsWithChildren,
} from "react";
import { twMerge } from "tailwind-merge";
@@ -104,7 +104,7 @@ export default function Dropdown({
className={twMerge(
"flex flex-col items-center relative",
"twui-dropdown-wrapper",
props.className
props.className,
)}
onMouseEnter={() => {
if (!hoverOpen) return;
@@ -142,7 +142,7 @@ export default function Dropdown({
className={twMerge(
"cursor-pointer",
"twui-dropdown-target",
targetWrapperProps?.className
targetWrapperProps?.className,
)}
>
{target}
@@ -155,20 +155,20 @@ export default function Dropdown({
position == "left"
? "left-[100%] top-[50%] -translate-y-[50%]"
: position == "right"
? "right-[100%] top-[50%] -translate-y-[50%]"
: position == "bottom-left"
? "left-0 top-[100%]"
: position == "bottom-right"
? "right-0 top-[100%]"
: position == "center"
? "left-[50%] -translate-x-[50%] top-[100%]"
: position == "top"
? "left-[50%] -translate-x-[50%] bottom-[100%]"
: "top-[100%]",
? "right-[100%] top-[50%] -translate-y-[50%]"
: position == "bottom-left"
? "left-0 top-[100%]"
: position == "bottom-right"
? "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",
"twui-dropdown-content",
contentWrapperProps?.className
contentWrapperProps?.className,
)}
onMouseEnter={() => {
if (!hoverOpen) return;
+4 -4
View File
@@ -1,4 +1,4 @@
import React, { ComponentProps, PropsWithChildren, ReactNode } from "react";
import type { ComponentProps, PropsWithChildren, ReactNode } from "react";
import { twMerge } from "tailwind-merge";
import Stack from "../layout/Stack";
import Border from "./Border";
@@ -46,7 +46,7 @@ export default function EmptyContent({
className={twMerge(
"w-full",
borderProps?.className,
"twui-empty-content-border"
"twui-empty-content-border",
)}
>
<Center>
@@ -57,7 +57,7 @@ export default function EmptyContent({
{...textProps}
className={twMerge(
"opacity-70 text-foreground-light dark:text-foreground-dark",
textProps?.className
textProps?.className,
)}
>
{title}
@@ -75,7 +75,7 @@ export default function EmptyContent({
className={twMerge(
"w-full",
"twui-empty-content-link",
linkProps?.className
linkProps?.className,
)}
href={url}
>
+3 -3
View File
@@ -1,6 +1,6 @@
import { ComponentProps, DetailedHTMLProps, HTMLAttributes } from "react";
import type { ComponentProps } from "react";
import Link from "../layout/Link";
import { TwuiHeaderLink } from "./HeaderNav";
import type { TwuiHeaderLink } from "./HeaderNav";
import { twMerge } from "tailwind-merge";
import Row from "../layout/Row";
@@ -21,7 +21,7 @@ export default function HeaderLink({ link, ...props }: TWUI_HEADER_LINK_PROPS) {
className={twMerge(
"grow p-2 hover:opacity-50",
"twui-header-link",
props.className
props.className,
)}
>
<Row>
+7 -3
View File
@@ -1,4 +1,8 @@
import React, { DetailedHTMLProps, HTMLAttributes, ReactNode } from "react";
import React, {
type DetailedHTMLProps,
type HTMLAttributes,
type ReactNode,
} from "react";
import { twMerge } from "tailwind-merge";
import Row from "../layout/Row";
import HeaderNavLinkComponent from "./HeaderNavLinkComponent";
@@ -44,13 +48,13 @@ export default function HeaderNav({
{...props}
className={twMerge(
"twui-header-nav w-full xl:w-auto",
props.className
props.className,
)}
>
<Row className="gap-x-2 gap-y-2 flex-col xl:flex-row items-start xl:items-stretch">
{headerLinks.map((link, index) => {
const targetCustomDropdown = customDropdowns?.find(
(d) => d.url == link.url
(d) => d.url == link.url,
);
return (
<HeaderNavLinkComponent
@@ -1,10 +1,10 @@
import React, { DetailedHTMLProps, HTMLAttributes, ReactNode } from "react";
import React, { type ReactNode } from "react";
import { twMerge } from "tailwind-merge";
import Row from "../layout/Row";
import HeaderLink from "./HeaderLink";
import { ChevronDown } from "lucide-react";
import Dropdown from "./Dropdown";
import { TwuiHeaderLink } from "./HeaderNav";
import type { TwuiHeaderLink } from "./HeaderNav";
import Card from "./Card";
import Stack from "../layout/Stack";
import Button from "../layout/Button";
@@ -31,7 +31,7 @@ export default function HeaderNavLinkComponent({
<ChevronDown
className={twMerge(
"hidden xl:flex xl:-ml-1",
"twui-header-nav-link-icon"
"twui-header-nav-link-icon",
)}
size={20}
/>
@@ -45,7 +45,7 @@ export default function HeaderNavLinkComponent({
<div
className={twMerge(
"relative w-full xl:w-auto [&_a.active]:font-bold",
"twui-header-nav-link-component"
"twui-header-nav-link-component",
)}
>
{isDropdown ? (
@@ -62,7 +62,7 @@ export default function HeaderNavLinkComponent({
>
<ChevronDown
className={twMerge(
"twui-header-nav-link-icon !text-link dark:!text-white"
"twui-header-nav-link-icon !text-link dark:!text-white",
)}
size={20}
/>
@@ -77,7 +77,7 @@ export default function HeaderNavLinkComponent({
<Card
className={twMerge(
"w-full p-0",
"twui-header-nav-link-dropdown"
"twui-header-nav-link-dropdown",
)}
>
<Stack className="w-full items-stretch gap-0 py-2">
@@ -90,7 +90,7 @@ export default function HeaderNavLinkComponent({
className="px-6 py-4"
/>
);
}
},
)}
</Stack>
</Card>
@@ -113,7 +113,7 @@ export default function HeaderNavLinkComponent({
<Card
className={twMerge(
"min-w-[200px] mt-2 p-0",
"twui-header-nav-link-dropdown"
"twui-header-nav-link-dropdown",
)}
>
<Stack className="w-full items-stretch gap-0 py-2">
+1 -1
View File
@@ -1,4 +1,4 @@
import { DetailedHTMLProps, HTMLAttributes } from "react";
import type { DetailedHTMLProps, HTMLAttributes } from "react";
import HtmlToReact from "html-to-react";
import { twMerge } from "tailwind-merge";
+10 -9
View File
@@ -1,8 +1,8 @@
import React, {
ComponentProps,
DetailedHTMLProps,
HTMLAttributes,
ReactNode,
type ComponentProps,
type DetailedHTMLProps,
type HTMLAttributes,
type ReactNode,
} from "react";
import { twMerge } from "tailwind-merge";
import Link from "../layout/Link";
@@ -25,15 +25,16 @@ export type TWUI_LINK_LIST_LINK_OBJECT = {
onClick?: React.MouseEventHandler<HTMLElement>;
};
export type TWUILink =
| TWUI_LINK_LIST_LINK_OBJECT
| TWUI_LINK_LIST_LINK_OBJECT[]
| undefined;
export type TWUI_LINK_LIST_PROPS = DetailedHTMLProps<
HTMLAttributes<HTMLDivElement>,
HTMLDivElement
> & {
links: (
| TWUI_LINK_LIST_LINK_OBJECT
| TWUI_LINK_LIST_LINK_OBJECT[]
| undefined
)[];
links: TWUILink[];
linkProps?: ComponentProps<typeof Link>;
buttonProps?: Omit<ComponentProps<typeof Button>, "title">;
divider?: boolean;
+1 -1
View File
@@ -1,4 +1,4 @@
import { DetailedHTMLProps, HTMLAttributes } from "react";
import type { DetailedHTMLProps, HTMLAttributes } from "react";
import { twMerge } from "tailwind-merge";
type Props = DetailedHTMLProps<
+2 -2
View File
@@ -1,4 +1,4 @@
import React, { DetailedHTMLProps, HTMLAttributes } from "react";
import type { DetailedHTMLProps, HTMLAttributes } from "react";
import { twMerge } from "tailwind-merge";
/**
@@ -15,7 +15,7 @@ export default function LoadingBlock({
"bg-slate-200 dark:bg-white/10",
"rounded animate-pulse w-full h-[60px]",
"twui-loading-block",
props.className
props.className,
)}
>
{props.children}
+1 -1
View File
@@ -1,4 +1,4 @@
import { ComponentProps, DetailedHTMLProps, HTMLAttributes } from "react";
import type { ComponentProps, DetailedHTMLProps, HTMLAttributes } from "react";
import { twMerge } from "tailwind-merge";
import Center from "../layout/Center";
import Loading from "./Loading";
+24 -13
View File
@@ -1,4 +1,4 @@
import React, { DetailedHTMLProps, HTMLAttributes } from "react";
import React, { type DetailedHTMLProps, type HTMLAttributes } from "react";
import ModalComponent from "../(partials)/ModalComponent";
import PopoverComponent from "../(partials)/PopoverComponent";
import { twMerge } from "tailwind-merge";
@@ -33,6 +33,7 @@ export type TWUI_MODAL_PROPS = DetailedHTMLProps<
debounce?: number;
onClose?: () => any;
hoverOpen?: boolean;
no_cancel_button?: boolean;
};
/**
@@ -43,6 +44,8 @@ export type TWUI_MODAL_PROPS = DetailedHTMLProps<
* @ID twui-popover-root
* @className twui-popover-content
* @className twui-popover-target
* @className twui-modal-content
* @className twui-modal-target
*/
export default function Modal(props: TWUI_MODAL_PROPS) {
const {
@@ -57,6 +60,7 @@ export default function Modal(props: TWUI_MODAL_PROPS) {
debounce = 500,
onClose,
hoverOpen,
no_cancel_button,
} = props;
const [ready, setReady] = React.useState(false);
@@ -94,23 +98,19 @@ export default function Modal(props: TWUI_MODAL_PROPS) {
const popoverTargetActiveRef = React.useRef(false);
const popoverContentActiveRef = React.useRef(false);
let closeTimeout: any;
const closeTimeoutRef = React.useRef<any>(null);
const popoverEnterFn = React.useCallback((e: any) => {
popoverTargetActiveRef.current = true;
popoverContentActiveRef.current = false;
window.clearTimeout(closeTimeoutRef.current);
setOpen(true);
props.onMouseEnter?.(e);
}, []);
const popoverLeaveFn = React.useCallback((e: any) => {
window.clearTimeout(closeTimeout);
closeTimeout = setTimeout(() => {
// if (popoverTargetActiveRef.current) {
// popoverTargetActiveRef.current = false;
// return;
// }
window.clearTimeout(closeTimeoutRef.current);
closeTimeoutRef.current = setTimeout(() => {
if (popoverContentActiveRef.current) {
popoverContentActiveRef.current = false;
return;
@@ -120,11 +120,22 @@ export default function Modal(props: TWUI_MODAL_PROPS) {
props.onMouseLeave?.(e);
}, []);
const target_class_name = isPopover
? "twui-popover-target"
: "twui-modal-target";
const content_class_name = isPopover
? "twui-popover-content"
: "twui-modal-content";
const handleClickOutside = React.useCallback((e: MouseEvent) => {
const targetEl = e.target as HTMLElement;
const closestWrapper = targetEl.closest(".twui-popover-content");
const closestTarget = targetEl.closest(".twui-popover-target");
if (!isPopover) {
return;
}
const closestWrapper = targetEl.closest(`.${content_class_name}`);
const closestTarget = targetEl.closest(`.${target_class_name}`);
if (closestTarget) return;
@@ -163,8 +174,8 @@ export default function Modal(props: TWUI_MODAL_PROPS) {
: targetWrapperProps?.onMouseLeave
}
className={twMerge(
"twui-popover-target",
targetWrapperProps?.className
target_class_name,
targetWrapperProps?.className,
)}
>
{target}
+37 -25
View File
@@ -1,4 +1,4 @@
import React, { ComponentProps, Dispatch, SetStateAction } from "react";
import type { ComponentProps, Dispatch, SetStateAction } from "react";
import _ from "lodash";
import { ChevronLeft, ChevronRight } from "lucide-react";
import { twMerge } from "tailwind-merge";
@@ -35,12 +35,30 @@ export default function Pagination({
const pages = Math.ceil(count / limit);
const next_button = (
<Button
title="Next Page Button"
onClick={() => {
window.scrollTo({ top: 0, behavior: "smooth" });
setPage?.((prev) => prev + 1);
}}
variant="outlined"
size="small"
className={twMerge(
"p-1",
isLimit ? "opacity-40 pointer-events-none" : "",
)}
>
<ChevronRight size={20} />
</Button>
);
return (
<Row
{...props}
className={twMerge(
"w-full justify-between flex-nowrap",
props.className
"w-full justify-between md:flex-nowrap",
props.className,
)}
>
{pages > 1 && (
@@ -54,21 +72,31 @@ export default function Pagination({
size="small"
className={twMerge(
"p-1",
page == 1 ? "opacity-40 pointer-events-none" : ""
page == 1 ? "opacity-40 pointer-events-none" : "",
)}
>
<ChevronLeft size={20} />
</Button>
)}
<Row className={twMerge("gap-6 w-full flex-nowrap justify-center")}>
<Span size="small" variant="faded">
{pages > 1 && <div className="flex md:hidden">{next_button}</div>}
<Row
className={twMerge(
"gap-6 w-full md:flex-nowrap justify-center",
)}
>
<Span
size="small"
variant="faded"
className="whitespace-nowrap"
>
Page {page} / {pages}
</Span>
{pages > 1 && (
<Row
className={twMerge(
"flex-nowrap overflow-x-auto p-1 max-w-[90%]"
"flex-nowrap overflow-x-auto p-1 max-w-[90%]",
)}
>
{Array(pages)
@@ -92,7 +120,7 @@ export default function Pagination({
size="small"
color={isCurrent ? "primary" : "gray"}
className={twMerge(
"p-1 w-6 h-6 min-w-6"
"p-1 w-6 h-6 min-w-6",
)}
key={index}
>
@@ -104,23 +132,7 @@ export default function Pagination({
)}
</Row>
{pages > 1 && (
<Button
title="Next Page Button"
onClick={() => {
window.scrollTo({ top: 0, behavior: "smooth" });
setPage?.((prev) => prev + 1);
}}
variant="outlined"
size="small"
className={twMerge(
"p-1",
isLimit ? "opacity-40 pointer-events-none" : ""
)}
>
<ChevronRight size={20} />
</Button>
)}
{pages > 1 && <div className="hidden md:flex">{next_button}</div>}
</Row>
);
}
+6 -2
View File
@@ -1,4 +1,8 @@
import React, { DetailedHTMLProps, HTMLAttributes, RefObject } from "react";
import React, {
type DetailedHTMLProps,
type HTMLAttributes,
type RefObject,
} from "react";
import { twMerge } from "tailwind-merge";
/**
@@ -27,7 +31,7 @@ export default function Paper({
"border border-slate-200 dark:border-white/10 border-solid w-full",
"relative",
"twui-paper",
props.className
props.className,
)}
>
{props.children}
+1 -2
View File
@@ -1,5 +1,4 @@
import React, { DetailedHTMLProps, HTMLAttributes } from "react";
import Modal, { TWUI_MODAL_PROPS } from "./Modal";
import Modal, { type TWUI_MODAL_PROPS } from "./Modal";
/**
* # Popover Component
+1 -1
View File
@@ -2,7 +2,7 @@ import React from "react";
import { serialize } from "next-mdx-remote/serialize";
import remarkGfm from "remark-gfm";
import rehypePrismPlus from "rehype-prism-plus";
import { MDXRemote, MDXRemoteSerializeResult } from "next-mdx-remote";
import { MDXRemote, type MDXRemoteSerializeResult } from "next-mdx-remote";
import { useMDXComponents } from "../mdx/mdx-components";
export const TWUIPrismLanguages = ["shell", "javascript"] as const;
+44 -33
View File
@@ -1,9 +1,13 @@
import { twMerge } from "tailwind-merge";
import Input, { InputProps } from "../form/Input";
import Input from "../form/Input";
import Button from "../layout/Button";
import Row from "../layout/Row";
import { Search as SearchIcon } from "lucide-react";
import React, { DetailedHTMLProps } from "react";
import React, {
type ComponentProps,
type DetailedHTMLProps,
type ReactNode,
} from "react";
let timeout: any;
@@ -14,14 +18,13 @@ export type SearchProps<KeyType extends string> = DetailedHTMLProps<
dispatch?: (value?: string) => void;
changeHandler?: (value?: string) => void;
delay?: number;
inputProps?: InputProps<KeyType>;
buttonProps?: DetailedHTMLProps<
React.ButtonHTMLAttributes<HTMLButtonElement>,
HTMLButtonElement
>;
inputProps?: ComponentProps<typeof Input>;
buttonProps?: Omit<ComponentProps<typeof Button>, "title">;
loading?: boolean;
placeholder?: string;
componentRef?: React.RefObject<HTMLInputElement | null>;
beforeSearchIconComponent?: ReactNode;
no_search_button?: boolean;
};
/**
@@ -39,10 +42,12 @@ export default function Search<KeyType extends string>({
loading,
placeholder,
componentRef,
beforeSearchIconComponent,
no_search_button,
...props
}: SearchProps<KeyType>) {
const [input, setInput] = React.useState(
props.defaultValue?.toString() || ""
props.defaultValue?.toString() || "",
);
React.useEffect(() => {
@@ -68,7 +73,7 @@ export default function Search<KeyType extends string>({
className={twMerge(
"relative xl:flex-nowrap items-stretch gap-0 flex-nowrap",
"twui-search-wrapper",
props?.className
props?.className,
)}
>
<Input
@@ -76,39 +81,45 @@ export default function Search<KeyType extends string>({
placeholder={placeholder || "Search"}
{...inputProps}
value={input}
onChange={(e) => setInput(e.target.value)}
onChange={(e) => {
setInput(e.target.value);
inputProps?.onChange?.(e as any);
}}
className={twMerge(
"rounded-r-none!",
"twui-search-input",
inputProps?.className
inputProps?.className,
)}
wrapperProps={{
className: "rounded-r-none!",
}}
componentRef={inputRef}
/>
<Button
loadingProps={{ size: "small" }}
{...buttonProps}
variant="outlined"
color="gray"
className={twMerge(
"rounded-l-none! ml-[1px]",
"twui-search-button",
buttonProps?.className
)}
onClick={() => {
dispatch?.(input);
changeHandler?.(input);
}}
title="Search Button"
loading={loading}
>
<SearchIcon
className="text-slate-800 dark:text-white"
size={20}
/>
</Button>
{beforeSearchIconComponent}
{no_search_button ? null : (
<Button
loadingProps={{ size: "small" }}
{...buttonProps}
variant="outlined"
color="gray"
className={twMerge(
"rounded-l-none! ml-[1px]",
"twui-search-button",
buttonProps?.className,
)}
onClick={() => {
dispatch?.(input);
changeHandler?.(input);
}}
title="Search Button"
loading={loading}
>
<SearchIcon
className="text-slate-800 dark:text-white"
size={15}
/>
</Button>
)}
</Row>
);
}
+2 -2
View File
@@ -1,4 +1,4 @@
import React, { DetailedHTMLProps, PropsWithChildren } from "react";
import React, { type DetailedHTMLProps, type PropsWithChildren } from "react";
import { twMerge } from "tailwind-merge";
type Props = PropsWithChildren &
@@ -15,7 +15,7 @@ export default function SingleLineCodeBlock({ children, ...props }: Props) {
"[&_.twui-code-block-header]:absolute [&_.twui-code-block-header]:!bg-transparent",
"[&_.twui-code-block-header]:mt-2 [&_.twui-code-block-header]:pr-3 [&_.twui-divider]:hidden",
"[&_pre]:!pr-14 [&_.twui-code-block-copied-text]:!hidden",
props.className
props.className,
)}
>
{children}
+2 -7
View File
@@ -1,10 +1,5 @@
import { LucideProps, Star } from "lucide-react";
import React, {
DetailedHTMLProps,
ForwardRefExoticComponent,
HTMLAttributes,
RefAttributes,
} from "react";
import { type LucideProps, Star } from "lucide-react";
import React, { type DetailedHTMLProps, type HTMLAttributes } from "react";
import { twMerge } from "tailwind-merge";
type StarProps = {
+6 -6
View File
@@ -16,13 +16,13 @@ export default function Table({ data }: Props) {
);
}
const headers = Object.keys(data[0]);
const headers = Object.keys(data[0]!);
return (
<div className={twMerge("overflow-x-auto w-full")}>
<table
className={twMerge(
"min-w-full divide-y divide-gray dark:divide-gray-dark"
"min-w-full divide-y divide-gray dark:divide-gray-dark",
)}
>
<thead className="bg-gray dark:bg-gray-dark">
@@ -32,7 +32,7 @@ export default function Table({ data }: Props) {
key={header}
className={twMerge(
"px-3 py-2 text-left opacity-50",
"font-semibold"
"font-semibold",
)}
title={header}
>
@@ -44,14 +44,14 @@ export default function Table({ data }: Props) {
<tbody
className={twMerge(
"bg-background-light dark:bg-background-dark",
"divide-y divide-gray dark:divide-gray-dark"
"divide-y divide-gray dark:divide-gray-dark",
)}
>
{data.map((row, index) => (
<tr
key={index}
className={twMerge(
"hover:bg-gray/20 dark:hover:bg-gray-dark/10"
"hover:bg-gray/20 dark:hover:bg-gray-dark/10",
)}
>
{headers.map((header) => (
@@ -60,7 +60,7 @@ export default function Table({ data }: Props) {
className={twMerge(
"px-3 py-2 whitespace-nowrap text-foreground-light",
"dark:text-foreground-dark max-w-[200px] overflow-hidden",
"overflow-ellipsis"
"overflow-ellipsis",
)}
title={row[header]}
>
+1 -1
View File
@@ -1,4 +1,4 @@
import React, { DetailedHTMLProps, HTMLAttributes, ReactNode } from "react";
import React, { type ReactNode } from "react";
import { twMerge } from "tailwind-merge";
import Border from "./Border";
import Stack from "../layout/Stack";
+13 -47
View File
@@ -1,20 +1,18 @@
import React, { PropsWithChildren } from "react";
import React, { type PropsWithChildren } from "react";
import { twMerge } from "tailwind-merge";
export type TWUITabsObject = {
title: string;
value: string;
content: React.ReactNode;
defaultActive?: boolean;
};
import {
ButtonClasses,
type TWUIButtonColorType,
type TWUIButtonVariantType,
} from "../layout/Button";
export type TWUI_TOGGLE_PROPS = PropsWithChildren &
React.DetailedHTMLProps<
React.HTMLAttributes<HTMLDivElement>,
HTMLDivElement
> & {
color?: "normal" | "secondary" | "error" | "success" | "gray";
variant?: "normal" | "outlined" | "ghost";
color?: TWUIButtonColorType;
variant?: TWUIButtonVariantType;
href?: string;
newTab?: boolean;
linkProps?: React.DetailedHTMLProps<
@@ -37,50 +35,18 @@ export default function Tag({
linkProps,
...props
}: TWUI_TOGGLE_PROPS) {
const finalClassName =
ButtonClasses[variant || "normal"]?.[color || "primary"] ?? "";
const mainComponent = (
<div
{...props}
className={twMerge(
"text-xs px-2 py-0.5 rounded-full outline-0",
"text-center flex items-center justify-center",
color == "secondary"
? "bg-secondary text-white outline-secbg-secondary"
: color == "success"
? "bg-success outline-success text-white"
: color == "error"
? "bg-orange-700 outline-orange-700"
: color == "gray"
? twMerge(
"bg-slate-100 outline-slate-200 dark:bg-gray-dark dark:outline-gray-dark",
"text-slate-800 dark:text-white"
)
: "bg-primary text-white outline-primbg-primary twui-tag-primary",
variant == "outlined"
? "!bg-transparent outline-1 " +
(color == "secondary"
? "text-secondary"
: color == "success"
? "text-success dark:text-success-dark"
: color == "error"
? "text-orange-700"
: color == "gray"
? "text-slate-700 dark:text-white/80"
: "text-primary dark:text-primary-dark twui-tag-primary-outlined")
: variant == "ghost"
? "!bg-transparent outline-none border-none " +
(color == "secondary"
? "text-secondary"
: color == "success"
? "text-success dark:text-success-dark"
: color == "error"
? "text-orange-700"
: color == "gray"
? "text-slate-700 dark:text-white/80"
: "text-primary dark:text-primary-dark")
: "",
finalClassName,
"twui-tag",
props.className
props.className,
)}
>
{children}
+1 -1
View File
@@ -1,4 +1,4 @@
import React, { DetailedHTMLProps, HTMLAttributes } from "react";
import React, { type DetailedHTMLProps, type HTMLAttributes } from "react";
import { twMerge } from "tailwind-merge";
import Card from "./Card";
import { X } from "lucide-react";
+29 -8
View File
@@ -1,16 +1,21 @@
import { DetailedHTMLProps, HTMLAttributes } from "react";
import {
type DetailedHTMLProps,
type HTMLAttributes,
useEffect,
useState,
} from "react";
import { twMerge } from "tailwind-merge";
export type TWUI_TOGGLE_PROPS = DetailedHTMLProps<
HTMLAttributes<HTMLDivElement>,
HTMLDivElement
> & {
active?: boolean;
setActive?: React.Dispatch<React.SetStateAction<boolean | undefined>>;
circleProps?: DetailedHTMLProps<
HTMLAttributes<HTMLDivElement>,
HTMLDivElement
>;
default_active?: boolean;
changeHandler?: (checked: boolean) => void;
};
/**
@@ -20,10 +25,24 @@ export type TWUI_TOGGLE_PROPS = DetailedHTMLProps<
*/
export default function Toggle({
circleProps,
active,
setActive,
default_active,
changeHandler,
...props
}: TWUI_TOGGLE_PROPS) {
const [active, setActive] = useState(default_active || false);
const [ready, setReady] = useState(false);
useEffect(() => {
if (!ready) return;
changeHandler?.(active);
}, [active]);
useEffect(() => {
setTimeout(() => {
setReady(true);
}, 200);
}, []);
return (
<div
{...props}
@@ -32,9 +51,11 @@ export default function Toggle({
"border border-slate-300 dark:border-white/30 border-solid rounded-full",
active ? "justify-end" : "justify-start",
"twui-toggle-wrapper",
props.className
props.className,
)}
onClick={() => setActive?.(!active)}
onClick={() => {
setActive(!active);
}}
>
{typeof active == "undefined" ? (
<div className="w-3.5 h-3.5 twui-toggle-circle"></div>
@@ -47,7 +68,7 @@ export default function Toggle({
? "bg-blue-600 dark:bg-blue-500"
: "bg-slate-300 dark:bg-white/40",
"twui-toggle-circle",
circleProps?.className
circleProps?.className,
)}
></div>
)}
@@ -1,10 +1,11 @@
import { Send, X } from "lucide-react";
import React, { Dispatch, SetStateAction } from "react";
import { ChatCompletionMessageParam } from "openai/resources/index";
import React, { type Dispatch, type SetStateAction } from "react";
import type { ChatCompletionMessageParam } from "openai/resources/index";
import Row from "../../layout/Row";
import Button from "../../layout/Button";
import CopySlug from "../CopySlug";
import AIPromptHistoryModal from "./AIPromptHistoryModal";
import AIVoiceTranscription from "./AIVoiceTranscription";
type Props = {
streamRes: string;
@@ -12,10 +13,15 @@ type Props = {
setPrompt: Dispatch<SetStateAction<string>>;
loading: boolean;
promptFn: (prompt: string) => void;
voice_transcription_fn?: (params: { last_message?: string }) => void;
transcribing?: boolean;
history: ChatCompletionMessageParam[];
prompt: string;
currentPromptRef: React.MutableRefObject<string>;
promptInputRef: React.RefObject<HTMLTextAreaElement>;
user_image_url?: string;
last_response?: string;
cancel_voice_transcription_fn?: () => void;
};
export default function AIPromptActionSection({
@@ -28,10 +34,24 @@ export default function AIPromptActionSection({
setPrompt,
currentPromptRef,
promptInputRef,
user_image_url,
transcribing,
voice_transcription_fn,
last_response,
cancel_voice_transcription_fn,
}: Props) {
return (
<Row className="w-full justify-between">
<Row className="gap-4">
<Row className="gap-4 px-2">
<AIVoiceTranscription
transcribing={transcribing}
voice_transcription_fn={voice_transcription_fn}
cancel_voice_transcription_fn={
cancel_voice_transcription_fn
}
history={history}
last_response={last_response}
/>
{streamRes.match(/./) && (
<React.Fragment>
<Button
@@ -53,10 +73,13 @@ export default function AIPromptActionSection({
/>
</React.Fragment>
)}
<AIPromptHistoryModal
history={history}
user_image_url={user_image_url}
/>
</Row>
<Row>
<AIPromptHistoryModal history={history} />
</Row>
<Row>
<Button
title="Send Prompt"
@@ -74,6 +97,7 @@ export default function AIPromptActionSection({
promptFn(prompt);
}}
loadingProps={{ size: "smaller" }}
disabled={transcribing}
/>
</Row>
</Row>
+13 -45
View File
@@ -1,15 +1,13 @@
import { ChatCompletionMessageParam } from "openai/resources/index";
import type { ChatCompletionMessageParam } from "openai/resources/index";
import React from "react";
import Paper from "../Paper";
import Stack from "../../layout/Stack";
import AIPromptPreview from "./AIPromptPreview";
import LoadingOverlay from "../LoadingOverlay";
import Textarea from "../../form/Textarea";
import AIPromptActionSection from "./AIPromptActionSection";
import Card from "../Card";
import Row from "../../layout/Row";
import Span from "../../layout/Span";
import { MessageCircleMore } from "lucide-react";
import AIPromptBlockInputSection from "./AIPromptBlockInputSection";
type Props = {
model?: string;
@@ -57,47 +55,17 @@ export default function AIPromptBlock({
history={history}
loading={loading}
/>
<Stack className="w-full relative">
{loading && <LoadingOverlay />}
<Textarea
placeholder={
placeholder ||
(model ? `Prompt ${model}` : "Prompt AI")
}
wrapperProps={{ className: "outline-none" }}
wrapperWrapperProps={{ className: "w-full" }}
value={prompt}
onChange={(e) => {
setPrompt(e.target.value);
}}
onKeyDown={(e) => {
if (e.key == "Enter" && !e.ctrlKey && !e.shiftKey) {
e.preventDefault();
currentPromptRef.current = prompt;
setTimeout(() => {
setPrompt("");
if (promptInputRef.current) {
promptInputRef.current.value = "";
}
}, 200);
promptFn(prompt);
}
}}
componentRef={promptInputRef}
// autoFocus
/>
<AIPromptActionSection
loading={loading}
promptFn={promptFn}
setStreamRes={setMdRes}
streamRes={mdRes}
history={history}
prompt={prompt}
setPrompt={setPrompt}
currentPromptRef={currentPromptRef}
promptInputRef={promptInputRef as any}
/>
</Stack>
<AIPromptBlockInputSection
{...{
model,
promptFn,
history,
loading,
mdRes,
setMdRes,
placeholder,
}}
/>
</Stack>
</Paper>
);
@@ -0,0 +1,73 @@
import type { ChatCompletionMessageParam } from "openai/resources/index";
import React from "react";
import Stack from "../../layout/Stack";
import LoadingOverlay from "../LoadingOverlay";
import Textarea from "../../form/Textarea";
import AIPromptActionSection from "./AIPromptActionSection";
type Props = {
model?: string;
promptFn: (prompt: string) => void;
history?: ChatCompletionMessageParam[];
loading?: boolean;
mdRes?: string;
setMdRes: React.Dispatch<React.SetStateAction<string>>;
placeholder?: string;
};
export default function AIPromptBlockInputSection({
model,
promptFn,
history = [],
loading = false,
mdRes = "",
setMdRes,
placeholder,
}: Props) {
const [prompt, setPrompt] = React.useState("");
const currentPromptRef = React.useRef("");
const promptInputRef = React.useRef<HTMLTextAreaElement>(null);
return (
<Stack className="w-full relative">
{loading && <LoadingOverlay />}
<Textarea
placeholder={
placeholder || (model ? `Prompt ${model}` : "Prompt AI")
}
wrapperProps={{ className: "outline-none" }}
wrapperWrapperProps={{ className: "w-full" }}
value={prompt}
onChange={(e) => {
setPrompt(e.target.value);
}}
onKeyDown={(e) => {
if (e.key == "Enter" && !e.ctrlKey && !e.shiftKey) {
e.preventDefault();
currentPromptRef.current = prompt;
setTimeout(() => {
setPrompt("");
if (promptInputRef.current) {
promptInputRef.current.value = "";
}
}, 200);
promptFn(prompt);
}
}}
componentRef={promptInputRef}
// autoFocus
/>
<AIPromptActionSection
loading={loading}
promptFn={promptFn}
setStreamRes={setMdRes}
streamRes={mdRes}
history={history}
prompt={prompt}
setPrompt={setPrompt}
currentPromptRef={currentPromptRef}
promptInputRef={promptInputRef as any}
/>
</Stack>
);
}
+39 -22
View File
@@ -1,7 +1,6 @@
import React from "react";
import { ChatCompletionMessageParam } from "openai/resources/index";
import type { ChatCompletionMessageParam } from "openai/resources/index";
import { twMerge } from "tailwind-merge";
import { Bot, User } from "lucide-react";
import { Bot, History, User } from "lucide-react";
import Modal from "../Modal";
import Button from "../../layout/Button";
import Stack from "../../layout/Stack";
@@ -11,13 +10,20 @@ import Divider from "../../layout/Divider";
import Row from "../../layout/Row";
import Card from "../Card";
import Border from "../Border";
import MarkdownEditorPreviewComponent from "../../mdx/markdown/MarkdownEditorPreviewComponent";
import ReactMarkdown from "react-markdown";
import remarkGfm from "remark-gfm";
import Img from "../../layout/Img";
type Props = {
history: ChatCompletionMessageParam[];
user_image_url?: string;
};
export default function AIPromptHistoryModal({ history }: Props) {
export default function AIPromptHistoryModal({
history,
user_image_url,
}: Props) {
if (!history[0]) return null;
return (
@@ -27,9 +33,10 @@ export default function AIPromptHistoryModal({ history }: Props) {
title="View Chat History"
size="smaller"
color="gray"
variant="outlined"
variant="ghost"
className="p-1!"
>
View History
<History size={20} opacity={0.7} />
</Button>
}
className="max-w-[900px] bg-slate-100 dark:bg-white/5 xl:p-8"
@@ -49,22 +56,35 @@ export default function AIPromptHistoryModal({ history }: Props) {
return (
<Row
key={index}
className="w-full items-start justify-end"
className="w-full items-start justify-end flex-nowrap"
>
<Card
className={twMerge(
"bg-background-dark text-foreground-dark dark:!bg-background-light dark:text-foreground-light"
"bg-background-dark text-foreground-dark dark:!bg-background-light dark:text-foreground-light",
"twui-llm-user-prompt-history-block",
)}
>
{hst.content?.toString()}
<Stack className="twui-llm-chat-block">
{hst.content?.toString()}
</Stack>
</Card>
<Border className="w-10 h-10 rounded-full p-2 items-center justify-center">
<User />
</Border>
{user_image_url ? (
<Img
alt="Prompting user"
size={30}
circle
src={user_image_url}
/>
) : (
<Border className="w-10 h-10 rounded-full p-2 items-center justify-center">
<User />
</Border>
)}
</Row>
);
}
return (
<Row
key={index}
@@ -74,21 +94,18 @@ export default function AIPromptHistoryModal({ history }: Props) {
<Border
className={twMerge(
"w-10 h-10 rounded-full items-center justify-center bg-white p-2",
"dark:bg-background-dark"
"dark:bg-background-dark",
)}
>
<Bot />
</Border>
</Stack>
<Card className="grow overflow-x-auto xl:p-8">
<MarkdownEditorPreviewComponent
value={hst.content?.toString() || ""}
maxHeight="none"
wrapperProps={{
className:
"border-none p-0 ai-response-content w-full",
}}
/>
<Stack className="twui-llm-chat-block">
<ReactMarkdown remarkPlugins={[remarkGfm]}>
{hst.content?.toString()}
</ReactMarkdown>
</Stack>
</Card>
</Row>
);
+1 -1
View File
@@ -2,7 +2,7 @@ import Divider from "../../layout/Divider";
import Stack from "../../layout/Stack";
import React from "react";
import MarkdownEditorPreviewComponent from "../../mdx/markdown/MarkdownEditorPreviewComponent";
import { ChatCompletionMessageParam } from "openai/resources/index";
import type { ChatCompletionMessageParam } from "openai/resources/index";
type Props = {
streamRes: string;
@@ -0,0 +1,70 @@
import { Speech, Voicemail, X } from "lucide-react";
import Button from "../../layout/Button";
import { ChatCompletionMessageParam } from "openai/resources";
import Row from "../../layout/Row";
import Loading from "../Loading";
import Border from "../Border";
type Props = {
voice_transcription_fn?: (params: { last_message?: string }) => void;
transcribing?: boolean;
history?: ChatCompletionMessageParam[];
last_response?: string;
cancel_voice_transcription_fn?: () => void;
};
export default function AIVoiceTranscription({
transcribing,
voice_transcription_fn,
history,
last_response,
cancel_voice_transcription_fn,
}: Props) {
if (!voice_transcription_fn) return null;
if (transcribing) {
return (
<Border className="px-1.5! py-0.5!">
<Row className="gap-2!">
<Loading size="small" />
<Button
title="Cancel Transcription"
className="p-1!"
onClick={(e) => {
e.preventDefault();
cancel_voice_transcription_fn?.();
}}
variant="ghost"
>
<X size={20} opacity={0.7} />
</Button>
</Row>
</Border>
);
}
return (
<Button
title="Listen to Speech"
size="smaller"
color="gray"
variant="ghost"
loading={transcribing}
onClick={(e) => {
e.preventDefault();
const history_last_response = history?.find(
(h) => h.role == "assistant",
);
voice_transcription_fn({
last_message:
last_response ||
history_last_response?.content?.toString(),
});
}}
className="p-1!"
>
<Speech size={20} opacity={0.7} />
</Button>
);
}