diff --git a/.gitignore b/.gitignore index d32cc78..8fd41ae 100644 --- a/.gitignore +++ b/.gitignore @@ -38,3 +38,4 @@ yarn-error.log* # typescript *.tsbuildinfo next-env.d.ts +dsql-schema-to-typedef.json \ No newline at end of file diff --git a/bun.lockb b/bun.lockb old mode 100644 new mode 100755 index 1ce9291..4134ef2 Binary files a/bun.lockb and b/bun.lockb differ diff --git a/components/lib/(partials)/ModalComponent.tsx b/components/lib/(partials)/ModalComponent.tsx index ff71a83..00c999d 100644 --- a/components/lib/(partials)/ModalComponent.tsx +++ b/components/lib/(partials)/ModalComponent.tsx @@ -40,7 +40,7 @@ export default function ModalComponent({ open, setOpen, ...props }: Props) { ; + editorRef?: MutableRefObject; readOnly?: boolean; /** Function to call when Ctrl+Enter is pressed */ ctrlEnterFn?: (editor: AceAjax.Editor) => void; content?: string; placeholder?: string; + title?: string; mode?: (typeof AceEditorModes)[number]; fontSize?: string; previewMode?: boolean; @@ -18,7 +20,9 @@ export type AceEditorComponentType = { React.HTMLAttributes, HTMLDivElement >; - refresh?: number; + refreshDepArr?: any[]; + editorOptions?: AceEditorOptions; + showLabel?: boolean; }; let timeout: any; @@ -40,13 +44,15 @@ export default function AceEditor({ previewMode, onChange, delay = 500, - refresh: externalRefresh, + refreshDepArr, wrapperProps, + editorOptions, + showLabel, + title, }: AceEditorComponentType) { try { - const editorElementRef = React.useRef(null); - // const editorRefInstance = React.useRef(null); - const editorRefInstance = React.useRef(null); + const editorElementRef = React.useRef(undefined); + const editorRefInstance = React.useRef(undefined); const [refresh, setRefresh] = React.useState(0); const [darkMode, setDarkMode] = React.useState(false); @@ -84,9 +90,7 @@ export default function AceEditor({ showLineNumbers: previewMode ? false : true, wrap: true, wrapMethod: "code", - // onchange: (e) => { - // console.log(e); - // }, + ...editorOptions, }); editor.commands.addCommand({ @@ -103,7 +107,9 @@ export default function AceEditor({ clearTimeout(timeout); setTimeout(() => { - onChange(editor.getValue()); + try { + onChange(editor.getValue()); + } catch (error) {} }, delay); } }); @@ -114,7 +120,7 @@ export default function AceEditor({ return function () { editor.destroy(); }; - }, [refresh, darkMode, ready, externalRefresh]); + }, [refresh, darkMode, ready, mode, ...(refreshDepArr || [])]); React.useEffect(() => { const htmlClassName = document.documentElement.className; @@ -129,12 +135,23 @@ export default function AceEditor({
+ {showLabel && title ? ( + + ) : null}
{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({ > {link.icon} - {link.title} + {link.component || link.title} {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} diff --git a/components/lib/elements/Loading.tsx b/components/lib/elements/Loading.tsx index d93ea9e..608a8e3 100644 --- a/components/lib/elements/Loading.tsx +++ b/components/lib/elements/Loading.tsx @@ -31,14 +31,18 @@ export default function Loading({ size, svgClassName, ...props }: Props) { })(); return ( -
+
diff --git a/components/lib/elements/Modal.tsx b/components/lib/elements/Modal.tsx index 2ca1783..e617152 100644 --- a/components/lib/elements/Modal.tsx +++ b/components/lib/elements/Modal.tsx @@ -146,7 +146,11 @@ export default function Modal(props: TWUI_MODAL_PROPS) { {target ? (
setOpen(!open)} + onClick={(e) => { + e.preventDefault(); + e.stopPropagation(); + setOpen(!open); + }} ref={finalTargetRef} onMouseEnter={ isPopover && (trigger === "hover" || hoverOpen) diff --git a/components/lib/elements/RemoteCodeBlock.tsx b/components/lib/elements/RemoteCodeBlock.tsx index 5a7c3d5..b35fcc8 100644 --- a/components/lib/elements/RemoteCodeBlock.tsx +++ b/components/lib/elements/RemoteCodeBlock.tsx @@ -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>(); @@ -31,7 +32,7 @@ export default function RemoteCodeBlock({ content }: Props) { }).then((mdxSrc) => { setMdxSource(mdxSrc); }); - }, []); + }, [refresh]); if (!mdxSource) { return null; diff --git a/components/lib/elements/Search.tsx b/components/lib/elements/Search.tsx index 36c2e7d..0f639c5 100644 --- a/components/lib/elements/Search.tsx +++ b/components/lib/elements/Search.tsx @@ -78,12 +78,12 @@ export default function Search({ 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({ variant="outlined" color="gray" className={twMerge( - "rounded-l-none ml-[1px]", + "rounded-l-none! ml-[1px]", "twui-search-button", buttonProps?.className )} diff --git a/components/lib/elements/StarRating.tsx b/components/lib/elements/StarRating.tsx index 0dfab43..54ed9ae 100644 --- a/components/lib/elements/StarRating.tsx +++ b/components/lib/elements/StarRating.tsx @@ -14,6 +14,7 @@ type StarProps = { starProps?: LucideProps; allowRating?: boolean; setValueExternal?: React.Dispatch>; + 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 ( @@ -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>; setSelectedStarValue: React.Dispatch>; starClicked: React.MutableRefObject; sectionHovered: React.MutableRefObject; selectedStarValue: number; + isActive: boolean; }) { - const isActive = index < finalValue; - return (
{ if (!allowRating) return; - setFinalValue(index + 1); }} onMouseLeave={() => { @@ -145,6 +148,7 @@ function StarComponent({ starClicked.current = true; setSelectedStarValue(index + 1); + changeHandler?.(index + 1); }} > diff --git a/components/lib/elements/Tabs.tsx b/components/lib/elements/Tabs.tsx index b936772..6da7138 100644 --- a/components/lib/elements/Tabs.tsx +++ b/components/lib/elements/Tabs.tsx @@ -27,6 +27,8 @@ export type TWUI_TOGGLE_PROPS = React.ComponentProps & { switchComponent?: ReactNode; setActiveValue?: React.Dispatch>; 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 ( {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); diff --git a/components/lib/elements/Toast.tsx b/components/lib/elements/Toast.tsx index 6440049..c8750f3 100644 --- a/components/lib/elements/Toast.tsx +++ b/components/lib/elements/Toast.tsx @@ -14,6 +14,7 @@ export type TWUIToastProps = DetailedHTMLProps< > & { open?: boolean; setOpen?: React.Dispatch>; + 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); }} > { + e.preventDefault(); + e.stopPropagation(); setOpen?.(false); + closeDispatch?.(open); }} > - {props.children} + + {props.children} + , - document.getElementById(IDName) as HTMLElement + document.getElementById(IDName) as HTMLElement, ); } diff --git a/components/lib/elements/ai/AIPromptHistoryModal.tsx b/components/lib/elements/ai/AIPromptHistoryModal.tsx index 074051d..bcb6bd4 100644 --- a/components/lib/elements/ai/AIPromptHistoryModal.tsx +++ b/components/lib/elements/ai/AIPromptHistoryModal.tsx @@ -32,7 +32,7 @@ export default function AIPromptHistoryModal({ history }: Props) { View History } - 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" > diff --git a/components/lib/elements/ai/AIPromptPreview.tsx b/components/lib/elements/ai/AIPromptPreview.tsx index a172544..dce06d2 100644 --- a/components/lib/elements/ai/AIPromptPreview.tsx +++ b/components/lib/elements/ai/AIPromptPreview.tsx @@ -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 = { diff --git a/components/lib/elements/lucide-icon.tsx b/components/lib/elements/lucide-icon.tsx new file mode 100644 index 0000000..48c5d0a --- /dev/null +++ b/components/lib/elements/lucide-icon.tsx @@ -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 ; +} diff --git a/components/lib/form/Checkbox.tsx b/components/lib/form/Checkbox.tsx index d2baed6..acf69df 100644 --- a/components/lib/form/Checkbox.tsx +++ b/components/lib/form/Checkbox.tsx @@ -66,7 +66,7 @@ export default function Checkbox({ const finalSize = size || 20; const [checked, setChecked] = React.useState( - defaultChecked || externalChecked || false + defaultChecked || externalChecked || false, ); const finalTitle = title @@ -93,7 +93,7 @@ export default function Checkbox({ "flex items-start md:items-center gap-2 flex-wrap md:flex-nowrap", readOnly ? "opacity-70 pointer-events-none" : "", wrapperClassName, - wrapperProps?.className + wrapperProps?.className, )} onClick={() => { setChecked(!checked); @@ -108,7 +108,7 @@ export default function Checkbox({ ? "bg-primary twui-checkbox-checked text-white outline-slate-400" : "dark:outline-white/50 outline-2 -outline-offset-2 twui-checkbox-unchecked", "twui-checkbox", - props.className + props.className, )} style={{ minWidth: finalSize + "px", @@ -125,7 +125,7 @@ export default function Checkbox({ {...labelProps} className={twMerge( "select-none whitespace-normal md:whitespace-nowrap", - labelProps?.className + labelProps?.className, )} > {label || finalTitle} @@ -134,8 +134,8 @@ export default function Checkbox({ )}
{info && ( - - + + {info} diff --git a/components/lib/form/Form.tsx b/components/lib/form/Form.tsx index 7d7bd49..fb8308e 100644 --- a/components/lib/form/Form.tsx +++ b/components/lib/form/Form.tsx @@ -1,11 +1,12 @@ import _ from "lodash"; -import { DetailedHTMLProps, FormHTMLAttributes } from "react"; +import { DetailedHTMLProps, FormHTMLAttributes, RefObject } from "react"; import { twMerge } from "tailwind-merge"; type Props = DetailedHTMLProps, HTMLFormElement> & { submitHandler?: (e: React.FormEvent, data: T) => void; changeHandler?: (e: React.FormEvent, data: T) => void; + formRef?: RefObject; }; /** @@ -13,8 +14,8 @@ type Props = * @className twui-form */ export default function Form< - T extends { [key: string]: any } = { [key: string]: any } ->({ ...props }: Props) { + T extends { [key: string]: any } = { [key: string]: any }, +>({ formRef, ...props }: Props) { const finalProps = _.omit(props, ["submitHandler", "changeHandler"]); return ( @@ -23,7 +24,7 @@ export default function Form< className={twMerge( "flex flex-col items-stretch gap-2 w-full bg-transparent", "twui-form", - props.className + props.className, )} onSubmit={(e) => { e.preventDefault(); @@ -42,6 +43,7 @@ export default function Form< props.changeHandler?.(e, data); props.onChange?.(e); }} + ref={formRef} > {props.children} diff --git a/components/lib/form/ImageUpload.tsx b/components/lib/form/ImageUpload.tsx index e74bf36..779b3a3 100644 --- a/components/lib/form/ImageUpload.tsx +++ b/components/lib/form/ImageUpload.tsx @@ -10,13 +10,15 @@ import imageInputToBase64, { } from "../utils/form/imageInputToBase64"; import { twMerge } from "tailwind-merge"; import Tag from "../elements/Tag"; +import Input from "./Input"; +import Row from "../layout/Row"; type ImageUploadProps = DetailedHTMLProps< React.HTMLAttributes, HTMLDivElement > & { onChangeHandler?: ( - imgData: ImageInputToBase64FunctionReturn | undefined + imgData: ImageInputToBase64FunctionReturn | undefined, ) => any; fileInputProps?: DetailedHTMLProps< React.InputHTMLAttributes, @@ -45,6 +47,7 @@ type ImageUploadProps = DetailedHTMLProps< React.SetStateAction >; setLoading?: React.Dispatch>; + setImgURL?: React.Dispatch>; externalImage?: ImageInputToBase64FunctionReturn; restoreImageFn?: () => void; }; @@ -67,6 +70,7 @@ export default function ImageUpload({ multiple, restoreImageFn, setLoading, + setImgURL, ...props }: ImageUploadProps) { const [imageObject, setImageObject] = React.useState< @@ -74,6 +78,11 @@ export default function ImageUpload({ >(externalImage); const [src, setSrc] = React.useState(existingImageUrl); const inputRef = React.useRef(null); + const imageUrlRef = React.useRef(""); + + React.useEffect(() => { + setImgURL?.(src); + }, [src]); React.useEffect(() => { if (existingImageUrl) setSrc(existingImageUrl); @@ -84,7 +93,7 @@ export default function ImageUpload({ {...props} className={twMerge( "w-full h-[300px] overflow-hidden", - props?.className + props?.className, )} > @@ -157,10 +166,10 @@ export default function ImageUpload({ {...previewImageProps} /> )} - +
) : ( { const targetEl = e.target as HTMLElement | undefined; @@ -199,6 +208,30 @@ export default function ImageUpload({ {label || "Click to Upload Image"} + + { + imageUrlRef.current = value; + }} + showLabel + /> + + {existingImageUrl && (