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
+25 -18
View File
@@ -3,7 +3,7 @@ import { twMerge } from "tailwind-merge";
import ReactDOM from "react-dom";
import Button from "../layout/Button";
import { X } from "lucide-react";
import { TWUI_MODAL_PROPS } from "../elements/Modal";
import { type TWUI_MODAL_PROPS } from "../elements/Modal";
import Paper from "../elements/Paper";
import _ from "lodash";
@@ -15,15 +15,20 @@ type Props = TWUI_MODAL_PROPS & {
/**
* # Modal Main Component
*/
export default function ModalComponent({ open, setOpen, ...props }: Props) {
export default function ModalComponent({
open,
setOpen,
no_cancel_button,
...props
}: Props) {
if (!open) return null;
return ReactDOM.createPortal(
<div
className={twMerge(
"fixed z-[200] top-0 left-0 w-screen h-screen",
"fixed z-200 top-0 left-0 w-screen h-screen",
"flex flex-col items-center justify-center p-4",
"twui-modal-root"
"twui-modal-root",
)}
role="dialog"
aria-modal="true"
@@ -31,7 +36,7 @@ export default function ModalComponent({ open, setOpen, ...props }: Props) {
<div
className={twMerge(
"absolute top-0 left-0 bg-dark/80 z-0",
"w-screen h-screen"
"w-screen h-screen",
)}
onClick={(e) => {
setOpen(false);
@@ -43,23 +48,25 @@ export default function ModalComponent({ open, setOpen, ...props }: Props) {
"z-10 max-w-modal bg-background-light dark:bg-background-dark",
"w-full relative max-h-[95vh] overflow-y-auto",
"twui-modal-content",
props.className
props.className,
)}
>
{props.children}
<Button
className="absolute top-0 right-0 p-2"
variant="ghost"
color="gray"
onClick={() => {
setOpen(false);
}}
title="Close Modal Button"
>
<X size={30} />
</Button>
{!no_cancel_button && (
<Button
className="absolute top-0 right-0 p-2"
variant="ghost"
color="gray"
onClick={() => {
setOpen(false);
}}
title="Close Modal Button"
>
<X size={30} />
</Button>
)}
</Paper>
</div>,
document.getElementById("twui-modal-root") as HTMLElement
document.getElementById("twui-modal-root") as HTMLElement,
);
}
+7 -6
View File
@@ -1,7 +1,7 @@
import React from "react";
import { twMerge } from "tailwind-merge";
import ReactDOM from "react-dom";
import { TWUI_MODAL_PROPS } from "../elements/Modal";
import type { TWUI_MODAL_PROPS } from "../elements/Modal";
import Paper from "../elements/Paper";
import _ from "lodash";
import twuiGrabPopoverStyles from "../(functions)/popver/grab-popover-styles";
@@ -44,17 +44,18 @@ export default function PopoverComponent({
}
}, [open, targetElRef, position]);
let closeTimeout: any;
const closeTimeoutRef = React.useRef<any>(null);
const popoverEnterFn = React.useCallback(() => {
popoverContentActiveRef.current = true;
popoverTargetActiveRef.current = false;
window.clearTimeout(closeTimeoutRef.current);
setOpen(true);
}, []);
const popoverLeaveFn = React.useCallback(() => {
window.clearTimeout(closeTimeout);
closeTimeout = setTimeout(() => {
window.clearTimeout(closeTimeoutRef.current);
closeTimeoutRef.current = setTimeout(() => {
if (popoverTargetActiveRef.current) {
popoverTargetActiveRef.current = false;
return;
@@ -71,7 +72,7 @@ export default function PopoverComponent({
className={twMerge(
"max-w-[300px] z-[250]",
"twui-popover-content",
props.className
props.className,
)}
style={{ ...style, ...props.style }}
onMouseEnter={
@@ -110,6 +111,6 @@ export default function PopoverComponent({
/> */}
{props.children}
</Paper>,
document.getElementById("twui-popover-root") as HTMLElement
document.getElementById("twui-popover-root") as HTMLElement,
);
}
+38
View File
@@ -0,0 +1,38 @@
/**
* # EJSON parse string
*/
function parse(
string: string | null | number,
reviver?: (this: any, key: string, value: any) => any
): { [s: string]: any } | { [s: string]: any }[] | undefined {
if (!string) return undefined;
if (typeof string == "object") return string;
if (typeof string !== "string") return undefined;
try {
return JSON.parse(string, reviver);
} catch (error) {
return undefined;
}
}
/**
* # EJSON stringify object
*/
function stringify(
value: any,
replacer?: ((this: any, key: string, value: any) => any) | null,
space?: string | number
): string | undefined {
try {
return JSON.stringify(value, replacer || undefined, space);
} catch (error) {
return undefined;
}
}
const EJSON = {
parse,
stringify,
};
export default EJSON;
+1 -1
View File
@@ -19,7 +19,7 @@ This package contains a `base.css` file which has all the base css rules require
### Install packages
```sh
bun add lucide-react tailwind-merge html-to-react gray-matter mdx typescript lodash react-code-blocks react-responsive-modal next-mdx-remote remark-gfm rehype-prism-plus openai
bun add lucide-react tailwind-merge html-to-react gray-matter mdx typescript lodash react-code-blocks react-responsive-modal next-mdx-remote remark-gfm rehype-prism-plus openai ace-builds
```
```sh
+9 -8
View File
@@ -21,14 +21,14 @@
--color-primary-dark-outline: #4b4b4b;
--color-primary-dark-text: #ffffff;
--color-secondary: #000000;
--color-secondary-hover: #dddddd;
--color-secondary-outline: #dddddd;
--color-secondary-text: #dddddd;
--color-secondary-dark: #000000;
--color-secondary-dark-hover: #dddddd;
--color-secondary-dark-outline: #dddddd;
--color-secondary-dark-text: #dddddd;
--color-secondary: #2b4bf0;
--color-secondary-hover: #2b4bf0;
--color-secondary-outline: #2b4bf0;
--color-secondary-text: #2b4bf0;
--color-secondary-dark: #2b4bf0;
--color-secondary-dark-hover: #2b4bf0;
--color-secondary-dark-outline: #2b4bf0;
--color-secondary-dark-text: #2b4bf0;
--color-accent: #000000;
--color-accent-hover: #dddddd;
@@ -51,6 +51,7 @@
--color-error-dark: #e5484d;
--color-warning: #ff6900;
--color-warning-dark: #ff8c3a;
--color-link: #0051c9;
--color-link-dark: #548adb;
+2 -2
View File
@@ -1,5 +1,5 @@
import { DetailedHTMLProps, HTMLAttributes } from "react";
import { DocsLinkType } from ".";
import type { DetailedHTMLProps, HTMLAttributes } from "react";
import type { DocsLinkType } from ".";
import Stack from "../../layout/Stack";
import TWUIDocsLink from "./TWUIDocsLink";
import { twMerge } from "tailwind-merge";
+9 -9
View File
@@ -1,9 +1,9 @@
import React, {
AnchorHTMLAttributes,
ComponentProps,
DetailedHTMLProps,
type AnchorHTMLAttributes,
type ComponentProps,
type DetailedHTMLProps,
} from "react";
import { DocsLinkType } from ".";
import type { DocsLinkType } from ".";
import Stack from "../../layout/Stack";
import { twMerge } from "tailwind-merge";
import Row from "../../layout/Row";
@@ -45,11 +45,11 @@ export default function TWUIDocsLink({
React.useEffect(() => {
if (typeof window !== "undefined") {
const basePathMatch = window.location.pathname.includes(
docLink.href
docLink.href,
);
const isStrictMatch = Boolean(
linkRef.current?.getAttribute("data-strict")
linkRef.current?.getAttribute("data-strict"),
);
if (strict || isStrictMatch) {
@@ -79,7 +79,7 @@ export default function TWUIDocsLink({
"twui-docs-left-aside-link whitespace-nowrap",
"grow overflow-hidden overflow-ellipsis",
isActive ? "active" : "",
props.className
props.className,
)}
ref={linkRef}
data-strict={strict || docLink.strict}
@@ -93,7 +93,7 @@ export default function TWUIDocsLink({
color="gray"
className={twMerge(
"p-1 hover:opacity-100",
expand ? "rotate-180 opacity-30" : "opacity-70"
expand ? "rotate-180 opacity-30" : "opacity-70",
)}
onClick={() => setExpand(!expand)}
title="Docs Aside Links Dropdown Button"
@@ -107,7 +107,7 @@ export default function TWUIDocsLink({
<Stack
className={twMerge(
"gap-2 w-full pl-3",
childWrapperProps?.className
childWrapperProps?.className,
)}
{...childWrapperProps}
>
@@ -1,5 +1,5 @@
import React, { DetailedHTMLProps, HTMLAttributes } from "react";
import { DocsLinkType } from ".";
import React, { type DetailedHTMLProps, type HTMLAttributes } from "react";
import type { DocsLinkType } from ".";
import Stack from "../../layout/Stack";
import TWUIDocsLink from "./TWUIDocsLink";
import { twMerge } from "tailwind-merge";
@@ -28,7 +28,7 @@ export default function TWUIDocsRightAside({
if (!ready) return;
const headerHrefs = document.querySelectorAll(
".twui-docs-header-anchor"
".twui-docs-header-anchor",
);
const linksArr: DocsLinkType[] = [];
@@ -87,7 +87,7 @@ export default function TWUIDocsRightAside({
if (!links.length) return;
const headerHrefs = document.querySelectorAll(
"a.twui-docs-header-anchor"
"a.twui-docs-header-anchor",
);
const observer = new IntersectionObserver((entries) => {
@@ -96,7 +96,7 @@ export default function TWUIDocsRightAside({
const id = entry.target.id;
const link = document.querySelector(
`.twui-docs-right-aside a[href="#${id}"]`
`.twui-docs-right-aside a[href="#${id}"]`,
);
if (link) {
link.classList.add("active");
@@ -104,7 +104,7 @@ export default function TWUIDocsRightAside({
} else {
const id = entry.target.id;
const link = document.querySelector(
`.twui-docs-right-aside a[href="#${id}"]`
`.twui-docs-right-aside a[href="#${id}"]`,
);
if (link) {
link.classList.remove("active");
@@ -130,7 +130,7 @@ export default function TWUIDocsRightAside({
className={twMerge(
"pb-10 hidden xl:flex min-w-[150px] max-w-[200px]",
"sticky top-6 twui-docs-right-aside",
props.className
props.className,
)}
>
<Stack className="w-full overflow-hidden">
+3 -3
View File
@@ -1,4 +1,4 @@
import {
import type {
ComponentProps,
DetailedHTMLProps,
HTMLAttributes,
@@ -61,7 +61,7 @@ export default function TWUIDocs({
{...docsContentProps}
className={twMerge(
"items-start gap-8 w-full flex-nowrap",
docsContentProps?.className
docsContentProps?.className,
)}
>
<TWUIDocsAside
@@ -74,7 +74,7 @@ export default function TWUIDocs({
<div
className={twMerge(
"block twui-docs-content pl-0 xl:pl-6 grow",
"overflow-hidden"
"overflow-hidden",
)}
>
{children}
+176 -172
View File
@@ -1,172 +1,176 @@
import React, { MutableRefObject } from "react";
import { twMerge } from "tailwind-merge";
import AceEditorModes from "./ace-editor-modes";
import { AceEditorOptions } from "@moduletrace/datasquirel/dist/package-shared/types";
export type AceEditorComponentType = {
editorRef?: MutableRefObject<AceAjax.Editor | undefined>;
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;
onChange?: (value: string) => void;
delay?: number;
wrapperProps?: React.DetailedHTMLProps<
React.HTMLAttributes<HTMLDivElement>,
HTMLDivElement
>;
refreshDepArr?: any[];
editorOptions?: AceEditorOptions;
showLabel?: boolean;
};
let timeout: any;
/**
* # Powerful Ace Editor
* @note **NOTE** head scripts required
* @script `https://cdnjs.cloudflare.com/ajax/libs/ace/1.22.0/ace.min.js`
* @script `https://cdnjs.cloudflare.com/ajax/libs/ace/1.22.0/ext-language_tools.min.js`
*/
export default function AceEditor({
editorRef,
readOnly,
ctrlEnterFn,
content = "",
placeholder,
mode,
fontSize,
previewMode,
onChange,
delay = 500,
refreshDepArr,
wrapperProps,
editorOptions,
showLabel,
title,
}: AceEditorComponentType) {
try {
const editorElementRef = React.useRef<HTMLDivElement>(undefined);
const editorRefInstance = React.useRef<AceAjax.Editor>(undefined);
const [refresh, setRefresh] = React.useState(0);
const [darkMode, setDarkMode] = React.useState(false);
const [ready, setReady] = React.useState(false);
React.useEffect(() => {
if (!ready) return;
if (!ace?.edit || !editorElementRef.current) {
setTimeout(() => {
setRefresh((prev) => prev + 1);
}, 1000);
return;
}
const editor = ace.edit(editorElementRef.current);
editor.setOptions({
mode: `ace/mode/${mode ? mode : "javascript"}`,
theme: darkMode
? "ace/theme/tomorrow_night_eighties"
: "ace/theme/ace_light",
value: (() => {
try {
return JSON.stringify(JSON.parse(content), null, 4);
} catch (error) {
return content;
}
})(),
placeholder: placeholder ? placeholder : "",
enableBasicAutocompletion: true,
enableLiveAutocompletion: true,
readOnly: readOnly ? true : false,
fontSize: fontSize ? fontSize : null,
showLineNumbers: previewMode ? false : true,
wrap: true,
wrapMethod: "code",
...editorOptions,
});
editor.commands.addCommand({
name: "myCommand",
bindKey: { win: "Ctrl-Enter", mac: "Command-Enter" },
exec: function (editor) {
if (ctrlEnterFn) ctrlEnterFn(editor);
},
readOnly: true,
});
editor.getSession().on("change", function (e) {
if (onChange) {
clearTimeout(timeout);
setTimeout(() => {
try {
onChange(editor.getValue());
} catch (error) {}
}, delay);
}
});
editorRefInstance.current = editor;
if (editorRef) editorRef.current = editor;
return function () {
editor.destroy();
};
}, [refresh, darkMode, ready, mode, ...(refreshDepArr || [])]);
React.useEffect(() => {
const htmlClassName = document.documentElement.className;
if (htmlClassName.match(/dark/i)) setDarkMode(true);
setTimeout(() => {
setReady(true);
}, 200);
}, []);
return (
<React.Fragment>
<div
{...wrapperProps}
className={twMerge(
"w-full h-[400px] block rounded-default",
"border border-slate-200 border-solid relative",
"dark:border-white/20",
showLabel && title ? "pt-4" : "",
wrapperProps?.className,
)}
>
{showLabel && title ? (
<label
className={twMerge(
"bg-background-light dark:bg-background-dark text-xs",
"-top-3 left-2 px-2 py-1 absolute z-10",
)}
>
{title}
</label>
) : null}
<div
ref={editorElementRef as any}
className="w-full h-full"
></div>
</div>
</React.Fragment>
);
} catch (error: any) {
return (
<React.Fragment>
<span className="m-0">
Editor Error:{" "}
<b className="text-red-600">{error.message}</b>
</span>
</React.Fragment>
);
}
}
import React, { type RefObject } from "react";
import { twMerge } from "tailwind-merge";
import AceEditorModes from "./ace-editor-modes";
import type { Ace as AceAjax } from "ace-builds";
export type AceEditorComponentType = {
editorRef?: RefObject<AceAjax.Editor | undefined>;
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;
onChange?: (value: string, editor?: AceAjax.Editor) => void;
delay?: number;
wrapperProps?: React.DetailedHTMLProps<
React.HTMLAttributes<HTMLDivElement>,
HTMLDivElement
>;
editorOptions?: AceAjax.EditorOptions;
showLabel?: boolean;
};
let timeout: any;
/**
* # Powerful Ace Editor
* @note **NOTE** head scripts required
* @script `https://cdnjs.cloudflare.com/ajax/libs/ace/1.22.0/ace.min.js`
* @script `https://cdnjs.cloudflare.com/ajax/libs/ace/1.22.0/ext-language_tools.min.js`
*/
export default function AceEditor({
editorRef,
readOnly,
ctrlEnterFn,
content = "",
placeholder,
mode,
fontSize,
previewMode,
onChange,
delay = 500,
wrapperProps,
editorOptions,
showLabel,
title,
}: AceEditorComponentType) {
try {
const editorElementRef = React.useRef<HTMLDivElement>(undefined);
const editorRefInstance = React.useRef<AceAjax.Editor>(undefined);
const [refresh, setRefresh] = React.useState(0);
const [darkMode, setDarkMode] = React.useState(false);
const [ready, setReady] = React.useState(false);
React.useEffect(() => {
if (!ready) return;
// @ts-ignore
const ace = window.ace;
if (!ace?.edit || !editorElementRef.current) {
setTimeout(() => {
setRefresh((prev) => prev + 1);
}, 1000);
return;
}
// @ts-ignore
const editor = ace.edit(editorElementRef.current) as AceAjax.Editor;
editor.setOptions({
mode: `ace/mode/${mode ? mode : "javascript"}`,
theme: darkMode
? "ace/theme/tomorrow_night_eighties"
: "ace/theme/ace_light",
value: (() => {
try {
return JSON.stringify(JSON.parse(content), null, 4);
} catch (error) {
return content;
}
})(),
placeholder: placeholder ? placeholder : "",
enableBasicAutocompletion: true,
enableLiveAutocompletion: true,
readOnly: readOnly ? true : false,
fontSize: fontSize ? fontSize : undefined,
showLineNumbers: previewMode ? false : true,
wrap: true,
wrapMethod: "code",
...editorOptions,
});
editor.commands.addCommand({
name: "myCommand",
bindKey: { win: "Ctrl-Enter", mac: "Command-Enter" },
exec: function (editor) {
if (ctrlEnterFn) ctrlEnterFn(editor);
},
readOnly: true,
});
editor.getSession().on("change", function (e) {
if (onChange) {
clearTimeout(timeout);
setTimeout(() => {
try {
onChange(
editor.getValue(),
editorRefInstance.current,
);
} catch (error) {}
}, delay);
}
});
editorRefInstance.current = editor;
if (editorRef) editorRef.current = editor;
return function () {
editor.destroy();
};
}, [refresh, darkMode, ready, mode]);
React.useEffect(() => {
const htmlClassName = document.documentElement.className;
if (htmlClassName.match(/dark/i)) setDarkMode(true);
setTimeout(() => {
setReady(true);
}, 200);
}, []);
return (
<React.Fragment>
<div
{...wrapperProps}
className={twMerge(
"w-full h-[400px] block rounded-default",
"border border-border border-solid relative",
"dark:border-white/20",
showLabel && title ? "pt-4" : "",
wrapperProps?.className,
)}
>
{showLabel && title ? (
<label
className={twMerge(
"bg-background-light dark:bg-background-dark text-xs",
"-top-3 left-2 px-2 py-1 absolute z-10",
)}
>
{title}
</label>
) : null}
<div
ref={editorElementRef as any}
className="w-full h-full"
></div>
</div>
</React.Fragment>
);
} catch (error: any) {
return (
<React.Fragment>
<span className="m-0">
Editor Error: <b className="text-error">{error.message}</b>
</span>
</React.Fragment>
);
}
}
+28 -30
View File
@@ -1,5 +1,5 @@
import React, { ComponentProps, MutableRefObject } from "react";
import { RawEditorOptions, TinyMCE, Editor } from "./tinymce";
import React, { useCallback, type ComponentProps, type RefObject } from "react";
import { type Editor, type RawEditorOptions } from "./tinymce";
import { twMerge } from "tailwind-merge";
import twuiSlugToNormalText from "../../utils/slug-to-normal-text";
import Border from "../../elements/Border";
@@ -7,7 +7,7 @@ import useTinyMCE from "./useTinyMCE";
export type TinyMCEEditorProps<KeyType extends string> = {
options?: RawEditorOptions;
editorRef?: React.MutableRefObject<Editor | null>;
editorRef?: React.RefObject<Editor | null>;
setEditor?: React.Dispatch<React.SetStateAction<Editor>>;
wrapperProps?: React.DetailedHTMLProps<
React.HTMLAttributes<HTMLDivElement>,
@@ -20,11 +20,12 @@ export type TinyMCEEditorProps<KeyType extends string> = {
borderProps?: ComponentProps<typeof Border>;
defaultValue?: string;
name?: KeyType;
changeHandler?: (content: string) => void;
changeHandler?: (content: string, editor?: Editor) => void;
showLabel?: boolean;
useParentCSS?: boolean;
placeholder?: string;
refreshDependencyArray?: any[];
base_url?: string;
};
/**
@@ -45,11 +46,12 @@ export default function TinyMCEEditor<KeyType extends string>({
useParentCSS,
placeholder,
refreshDependencyArray,
base_url,
}: TinyMCEEditorProps<KeyType>) {
const { tinyMCE } = useTinyMCE();
const { tinyMCE } = useTinyMCE({ base_url });
const editorComponentRef = React.useRef<HTMLDivElement>(null);
const editorRef: MutableRefObject<Editor | null> =
const editorRef: RefObject<Editor | null> =
passedEditorRef || React.useRef(null);
const EDITOR_VALUE_CHANGE_TIMEOUT = 500;
@@ -59,7 +61,6 @@ export default function TinyMCEEditor<KeyType extends string>({
const [ready, setReady] = React.useState(false);
const [darkMode, setDarkMode] = React.useState(false);
const [refresh, setRefresh] = React.useState(0);
const [editor, setEditor] = React.useState<Editor>();
const title = name ? twuiSlugToNormalText(name) : "Rich Text";
@@ -81,13 +82,28 @@ export default function TinyMCEEditor<KeyType extends string>({
const id = crypto.randomUUID();
const updateContent = useCallback((editor: Editor) => {
if (changeHandler) {
window.clearTimeout(valueTimeout);
valueTimeout = setTimeout(() => {
changeHandler(
editor.getContent(),
editorRef.current || undefined,
);
}, EDITOR_VALUE_CHANGE_TIMEOUT);
}
}, []);
React.useEffect(() => {
if (!editorComponentRef.current || !themeReady || !tinyMCE) {
return;
}
const baseUrl =
base_url ||
process.env.NEXT_PUBLIC_TINYMCE_BASE_URL ||
process.env.BUNEXT_PUBLIC_TINYMCE_BASE_URL ||
"https://www.datasquirel.com/tinymce-public";
tinyMCE.init({
@@ -96,11 +112,10 @@ export default function TinyMCEEditor<KeyType extends string>({
plugins:
"advlist lists link image charmap preview anchor searchreplace visualblocks code fullscreen insertdatetime media table code help wordcount",
toolbar:
"undo redo | blocks | bold italic underline link image | bullist numlist outdent indent | removeformat code searchreplace wordcount preview insertdatetime",
"undo redo | blocks | bold italic underline link image | align bullist numlist | outdent indent removeformat code searchreplace wordcount preview insertdatetime",
content_style:
"body { font-family:Helvetica,Arial,sans-serif; font-size:14px; background-color: transparent }",
init_instance_callback: (editor) => {
setEditor(editor as any);
if (editorRef) {
editorRef.current = editor;
passedSetEditor?.(editor);
@@ -108,18 +123,12 @@ export default function TinyMCEEditor<KeyType extends string>({
if (defaultValue) editor.setContent(defaultValue);
setReady(true);
// editor.on("change", (e) => {
// changeHandler?.(editor.getContent());
// });
editor.on("change", (e) => {
updateContent(editor);
});
editor.on("input", (e) => {
if (changeHandler) {
window.clearTimeout(valueTimeout);
valueTimeout = setTimeout(() => {
changeHandler(editor.getContent());
}, EDITOR_VALUE_CHANGE_TIMEOUT);
}
updateContent(editor);
});
if (useParentCSS) {
@@ -149,14 +158,6 @@ export default function TinyMCEEditor<KeyType extends string>({
};
}, [tinyMCE, themeReady, refresh, ...(refreshDependencyArray || [])]);
React.useEffect(() => {
const instance = editorRef.current;
if (instance) {
instance.setContent(defaultValue || "");
}
}, [defaultValue]);
return (
<div
{...wrapperWrapperProps}
@@ -165,9 +166,6 @@ export default function TinyMCEEditor<KeyType extends string>({
"bg-background-light dark:bg-background-dark",
wrapperWrapperProps?.className,
)}
onInput={(e) => {
console.log(`Input Detected`);
}}
>
{showLabel && (
<label
File diff suppressed because it is too large Load Diff
+8 -2
View File
@@ -1,9 +1,13 @@
import React from "react";
import { TinyMCE } from "./tinymce";
import type { TinyMCE } from "./tinymce";
let interval: any;
export default function useTinyMCE() {
type Params = {
base_url?: string;
};
export default function useTinyMCE(params?: Params) {
const [tinyMCE, setTinyMCE] = React.useState<TinyMCE>();
const [refresh, setRefresh] = React.useState(0);
const [scriptLoaded, setScriptLoaded] = React.useState(false);
@@ -21,7 +25,9 @@ export default function useTinyMCE() {
const script = document.createElement("script");
const baseUrl =
params?.base_url ||
process.env.NEXT_PUBLIC_TINYMCE_BASE_URL ||
process.env.BUNEXT_PUBLIC_TINYMCE_BASE_URL ||
"https://www.datasquirel.com/tinymce-public";
script.src = `${baseUrl}/tinymce.min.js`;
+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>
);
}
+8 -5
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 CheckMarkSVG from "../svgs/CheckMarkSVG";
@@ -36,6 +36,7 @@ export type CheckboxProps = Omit<
noLabel?: boolean;
size?: number;
changeHandler?: (value: boolean) => void;
clickDispatchHandler?: (value: boolean) => void;
info?: string | ReactNode;
wrapperWrapperProps?: ComponentProps<typeof Stack>;
};
@@ -61,6 +62,7 @@ export default function Checkbox({
wrapperWrapperProps,
noLabel,
title,
clickDispatchHandler,
...props
}: CheckboxProps) {
const finalSize = size || 20;
@@ -95,9 +97,10 @@ export default function Checkbox({
wrapperClassName,
wrapperProps?.className,
)}
onClick={() => {
onClick={(e) => {
setChecked(!checked);
externalSetChecked?.(!checked);
clickDispatchHandler?.(!checked);
}}
>
<div
+8 -4
View File
@@ -1,11 +1,15 @@
import Button from "../layout/Button";
import Stack from "../layout/Stack";
import { FileArchive, FilePlus2, X } from "lucide-react";
import React, { ComponentProps, DetailedHTMLProps, ReactNode } from "react";
import React, {
type ComponentProps,
type DetailedHTMLProps,
type ReactNode,
} from "react";
import Card from "../elements/Card";
import Span from "../layout/Span";
import Center from "../layout/Center";
import { FileInputToBase64FunctionReturn } from "../utils/form/fileInputToBase64";
import type { FileInputToBase64FunctionReturn } from "../utils/form/fileInputToBase64";
import { twMerge } from "tailwind-merge";
import fileInputToBase64 from "../utils/form/fileInputToBase64";
import Row from "../layout/Row";
@@ -149,7 +153,7 @@ export default function FileUpload({
for (let i = 0; i < files.length; i++) {
const file = files[i];
const fileObj = await fileInputToBase64({
inputFile: file,
inputFile: file!,
});
filesArr.push(fileObj);
}
@@ -330,7 +334,7 @@ export default function FileUpload({
}
});
} else {
inputFile = e.dataTransfer.files?.[0];
inputFile = e.dataTransfer.files?.[0]!;
}
if (!inputFile) return;
+1 -1
View File
@@ -1,5 +1,5 @@
import _ from "lodash";
import { DetailedHTMLProps, FormHTMLAttributes, RefObject } from "react";
import type { DetailedHTMLProps, FormHTMLAttributes, RefObject } from "react";
import { twMerge } from "tailwind-merge";
type Props<T extends { [key: string]: any } = { [key: string]: any }> =
+53 -40
View File
@@ -1,12 +1,12 @@
import Button from "../layout/Button";
import Stack from "../layout/Stack";
import { ImagePlus, X } from "lucide-react";
import React, { DetailedHTMLProps } from "react";
import React, { type DetailedHTMLProps } from "react";
import Card from "../elements/Card";
import Span from "../layout/Span";
import Center from "../layout/Center";
import imageInputToBase64, {
ImageInputToBase64FunctionReturn,
type ImageInputToBase64FunctionReturn,
} from "../utils/form/imageInputToBase64";
import { twMerge } from "tailwind-merge";
import Tag from "../elements/Tag";
@@ -20,6 +20,9 @@ type ImageUploadProps = DetailedHTMLProps<
onChangeHandler?: (
imgData: ImageInputToBase64FunctionReturn | undefined,
) => any;
multipleOnChangeHandler?: (
imgData: ImageInputToBase64FunctionReturn[] | undefined,
) => any;
fileInputProps?: DetailedHTMLProps<
React.InputHTMLAttributes<HTMLInputElement>,
HTMLInputElement
@@ -50,6 +53,7 @@ type ImageUploadProps = DetailedHTMLProps<
setImgURL?: React.Dispatch<React.SetStateAction<string | undefined>>;
externalImage?: ImageInputToBase64FunctionReturn;
restoreImageFn?: () => void;
upload_only?: boolean;
};
/**
@@ -71,6 +75,8 @@ export default function ImageUpload({
restoreImageFn,
setLoading,
setImgURL,
multipleOnChangeHandler,
upload_only,
...props
}: ImageUploadProps) {
const [imageObject, setImageObject] = React.useState<
@@ -119,7 +125,7 @@ export default function ImageUpload({
});
imgArr.push(fileObj);
}
multipleOnChangeHandler?.(imgArr);
externalSetImages?.(imgArr);
setLoading?.(false);
})();
@@ -208,43 +214,50 @@ export default function ImageUpload({
<Span size="smaller" variant="faded">
{label || "Click to Upload Image"}
</Span>
<Stack className="cancel-upload w-full items-stretch gap-1">
<Input
placeholder="Eg. https://example.com/img.png"
className="text-sm twui-image-url-input"
title="Enter Image URL"
wrapperWrapperProps={{ className: "mt-2" }}
changeHandler={(value) => {
imageUrlRef.current = value;
}}
showLabel
/>
<Button
title="Restore Image Button"
size="smaller"
variant="outlined"
color="gray"
onClick={() => {
if (!imageUrlRef.current) return;
setSrc(imageUrlRef.current);
}}
>
Set Image URL
</Button>
</Stack>
{existingImageUrl && (
<Button
title="Restore Image Button"
size="smaller"
variant="ghost"
onClick={() => {
restoreImageFn?.() ||
setSrc(existingImageUrl);
}}
className="cancel-upload"
>
Restore Original Image
</Button>
{upload_only ? null : (
<>
<Stack className="cancel-upload w-full items-stretch gap-1">
<Input
placeholder="URL Eg. https://example.com/img.png"
className="text-sm twui-image-url-input"
title="Enter Image URL"
wrapperWrapperProps={{
className: "mt-2",
}}
changeHandler={(value) => {
imageUrlRef.current = value;
}}
/>
<Button
title="Restore Image Button"
size="smaller"
variant="outlined"
color="gray"
onClick={() => {
if (!imageUrlRef.current)
return;
setSrc(imageUrlRef.current);
}}
className="opacity-70"
>
Set Image URL
</Button>
</Stack>
{existingImageUrl && (
<Button
title="Restore Image Button"
size="smaller"
variant="ghost"
onClick={() => {
restoreImageFn?.() ||
setSrc(existingImageUrl);
}}
className="cancel-upload"
>
Restore Original Image
</Button>
)}
</>
)}
</Stack>
</Center>
+1 -1
View File
@@ -2,7 +2,7 @@ import React, { useEffect, useRef, useState } from "react";
import Row from "../../layout/Row";
import { Info, Minus, Plus } from "lucide-react";
import twuiNumberfy from "../../utils/numberfy";
import { InputProps } from ".";
import type { InputProps } from ".";
let pressInterval: any;
let pressTimeout: any;
+67 -19
View File
@@ -1,16 +1,16 @@
import React, {
ComponentProps,
DetailedHTMLProps,
InputHTMLAttributes,
LabelHTMLAttributes,
ReactNode,
RefObject,
TextareaHTMLAttributes,
type ComponentProps,
type DetailedHTMLProps,
type InputHTMLAttributes,
type LabelHTMLAttributes,
type ReactNode,
type RefObject,
type TextareaHTMLAttributes,
useRef,
} from "react";
import { twMerge } from "tailwind-merge";
import Span from "../../layout/Span";
import { Eye, EyeOff, Info, InfoIcon, X } from "lucide-react";
import { Copy, Eye, EyeOff, Info, InfoIcon, X } from "lucide-react";
import { AutocompleteOptions } from "../../types";
import twuiNumberfy from "../../utils/numberfy";
import Dropdown from "../../elements/Dropdown";
@@ -19,7 +19,7 @@ import NumberInputButtons from "./NumberInputButtons";
import twuiSlugToNormalText from "../../utils/slug-to-normal-text";
import Row from "../../layout/Row";
import Paper from "../../elements/Paper";
import { TWUISelectValidityObject } from "../Select";
import type { TWUISelectValidityObject } from "../Select";
export type InputProps<KeyType extends string> = Omit<
DetailedHTMLProps<InputHTMLAttributes<HTMLInputElement>, HTMLInputElement>,
@@ -75,6 +75,7 @@ export type InputProps<KeyType extends string> = Omit<
HTMLDivElement
>;
// refreshDefaultValue?: number;
copy?: boolean;
};
let refreshes = 0;
@@ -117,6 +118,7 @@ export default function Input<KeyType extends string>(
clearInputProps,
rawNumber,
// refreshDefaultValue,
copy,
...props
} = inputProps;
@@ -163,7 +165,7 @@ export default function Input<KeyType extends string>(
numberText ? "text" : props.type,
);
const DEFAULT_DEBOUNCE = 500;
const DEFAULT_DEBOUNCE = 200;
const finalDebounce = debounce || DEFAULT_DEBOUNCE;
const finalLabel =
label ||
@@ -225,11 +227,12 @@ export default function Input<KeyType extends string>(
}
};
const isControlled = props.value !== undefined && props.value !== null;
React.useEffect(() => {
// if (!existingReady) return;
if (typeof props.value !== "string" || !props.value.match(/./)) return;
setValue(String(props.value));
}, [props.value]);
if (!isControlled) return;
setValue(String(props.value ?? ""));
}, [props.value, isControlled]);
// React.useEffect(() => {
// if (!refreshDefaultValue) return;
@@ -290,8 +293,8 @@ export default function Input<KeyType extends string>(
onChange={handleValueChange}
autoComplete={autoComplete}
rows={props.height ? Number(props.height) : props.rows || 2}
defaultValue={defaultInitialValue}
value={props.value ? getFinalValue(props.value) : undefined}
defaultValue={isControlled ? undefined : defaultInitialValue}
value={isControlled ? getFinalValue(props.value) : undefined}
/>
) : (
<input
@@ -318,9 +321,9 @@ export default function Input<KeyType extends string>(
}}
onChange={handleValueChange}
type={inputType}
defaultValue={defaultInitialValue}
defaultValue={isControlled ? undefined : defaultInitialValue}
autoComplete={autoComplete}
value={props.value ? getFinalValue(props.value) : undefined}
value={isControlled ? getFinalValue(props.value) : undefined}
/>
);
@@ -393,7 +396,29 @@ export default function Input<KeyType extends string>(
{targetComponent}
{props.type == "search" || props.readOnly ? null : (
{copy ? (
<>
<div
title="Copy value"
className="cursor-pointer opacity-50 hover:opacity-100"
onClick={() => {
const text =
inputRef.current.value ||
textAreaRef.current.value;
if (text) {
navigator.clipboard
.writeText(text)
.then(() => {
// window.alert(`Text Copied`);
});
}
}}
>
<Copy size={17} />
</div>
</>
) : props.type == "search" || props.readOnly ? null : (
<div
title="Clear Input Field"
{...clearInputProps}
@@ -502,3 +527,26 @@ export default function Input<KeyType extends string>(
</Stack>
);
}
const updateInput = ({
newValue,
inputRef,
}: {
newValue: string;
inputRef: RefObject<HTMLInputElement & HTMLTextAreaElement>;
}) => {
const input = inputRef.current;
if (!input) return;
// Get the native setter (React overrides the default one)
const nativeInputValueSetter = Object.getOwnPropertyDescriptor(
window.HTMLInputElement.prototype,
"value",
)?.set;
// Call it directly on the input
nativeInputValueSetter?.call(input, newValue);
// Dispatch the event - React listens for 'input', not 'change'
input.dispatchEvent(new Event("input", { bubbles: true }));
};
+1 -1
View File
@@ -1,4 +1,4 @@
import {
import type {
ComponentProps,
DetailedHTMLProps,
InputHTMLAttributes,
File diff suppressed because it is too large Load Diff
+30 -26
View File
@@ -1,14 +1,14 @@
import { ChevronDown, Info, LucideProps } from "lucide-react";
import { ChevronDown, Info, type LucideProps } from "lucide-react";
import React, {
ComponentProps,
DetailedHTMLProps,
Dispatch,
InputHTMLAttributes,
LabelHTMLAttributes,
ReactNode,
RefObject,
SelectHTMLAttributes,
SetStateAction,
type ComponentProps,
type DetailedHTMLProps,
type Dispatch,
type InputHTMLAttributes,
type LabelHTMLAttributes,
type ReactNode,
type RefObject,
type SelectHTMLAttributes,
type SetStateAction,
} from "react";
import { twMerge } from "tailwind-merge";
import Row from "../layout/Row";
@@ -18,6 +18,8 @@ import Span from "../layout/Span";
import Stack from "../layout/Stack";
import twuiSlugify from "../utils/slugify";
import twuiSlugToNormalText from "../utils/slug-to-normal-text";
import Input from "./Input";
import Button from "../layout/Button";
export type TWUISelectValidityObject = {
isValid?: boolean;
@@ -26,7 +28,7 @@ export type TWUISelectValidityObject = {
export type TWUISelectOptionObject<
KeyType extends string = string,
T extends { [k: string]: any } = { [k: string]: any }
T extends { [k: string]: any } = { [k: string]: any },
> = {
title?: string;
value: KeyType;
@@ -36,7 +38,7 @@ export type TWUISelectOptionObject<
export type TWUISelectProps<
KeyType extends string,
T extends { [k: string]: any } = { [k: string]: any }
T extends { [k: string]: any } = { [k: string]: any },
> = DetailedHTMLProps<
SelectHTMLAttributes<HTMLSelectElement>,
HTMLSelectElement
@@ -60,11 +62,13 @@ export type TWUISelectProps<
validateValueFn?: (value: string) => Promise<TWUISelectValidityObject>;
dispatchState?: Dispatch<SetStateAction<T | undefined>>;
name?: KeyType;
input_props?: ComponentProps<typeof Input>;
option_button_props?: Omit<ComponentProps<typeof Button>, "title">;
};
export type TWUISelectValueObject<
KeyType extends string,
T extends { [k: string]: any } = { [k: string]: any }
T extends { [k: string]: any } = { [k: string]: any },
> = {
value: KeyType;
data?: T;
@@ -78,7 +82,7 @@ export type TWUISelectValueObject<
*/
export default function Select<
KeyType extends string,
T extends { [k: string]: any } = { [k: string]: any }
T extends { [k: string]: any } = { [k: string]: any },
>({
label,
options,
@@ -102,9 +106,9 @@ export default function Select<
const [value, setValue] = React.useState<TWUISelectValueObject<KeyType, T>>(
{
value: options[0]?.value,
value: options[0]?.value!,
data: options[0]?.data,
}
},
);
React.useEffect(() => {
@@ -128,10 +132,10 @@ export default function Select<
const selectID = label
? twuiSlugify(label)
: props.name
? twuiSlugify(props.name)
: props.title
? twuiSlugify(props.title)
: `select-${Math.round(Math.random() * 1000000)}`;
? twuiSlugify(props.name)
: props.title
? twuiSlugify(props.title)
: `select-${Math.round(Math.random() * 1000000)}`;
return (
<Stack
@@ -148,7 +152,7 @@ export default function Select<
"focus:outline-slate-700 dark:focus:outline-white/50",
"bg-white dark:bg-background-dark",
validity.isValid ? "" : "outline-warning border-warning",
wrapperProps?.className
wrapperProps?.className,
)}
>
{showLabel && (
@@ -159,7 +163,7 @@ export default function Select<
"text-xs absolute -top-2.5 left-2 text-foreground-light/80 bg-background-light",
"dark:text-foreground-dark/70 dark:bg-background-dark px-1.5 rounded-t",
"twui-input-label",
labelProps?.className
labelProps?.className,
)}
>
{label || props.title || props.name}
@@ -174,7 +178,7 @@ export default function Select<
"w-full pl-3 py-2 rounded-default appearance-none pr-8",
"grow !border-none !outline-none",
"twui-select",
props.className
props.className,
)}
ref={selectRef}
value={
@@ -183,7 +187,7 @@ export default function Select<
}
onChange={(e) => {
const targetValue = options.find(
(opt) => opt.value == e.target.value
(opt) => opt.value == e.target.value,
);
if (targetValue) {
@@ -192,7 +196,7 @@ export default function Select<
changeHandler?.(
e.target.value as (typeof options)[number]["value"],
targetValue?.data
targetValue?.data,
);
props.onChange?.(e);
@@ -219,7 +223,7 @@ export default function Select<
{...iconProps}
className={twMerge(
"pointer-events-none -ml-6",
iconProps?.className
iconProps?.className,
)}
/>
+1 -1
View File
@@ -1,4 +1,4 @@
import Input, { InputProps } from "./Input";
import Input, { type InputProps } from "./Input";
/**
* # Textarea Component
+1 -1
View File
@@ -29,7 +29,7 @@ export default function useIntersectionObserver({
const entry = entries[0];
window.clearTimeout(timeoutRef.current);
if (entry.isIntersecting) {
if (entry?.isIntersecting) {
timeoutRef.current = setTimeout(() => {
setIsIntersecting(true);
+11 -2
View File
@@ -1,14 +1,20 @@
import React from "react";
export type TWUIErrorType = {
msg?: string;
};
type Params = {
initialLoading?: boolean;
initialReady?: boolean;
initialOpen?: boolean;
};
export type UseStatusStatusType = {
export type TWUIStatusType = {
msg?: string;
error?: boolean;
success?: boolean;
loading?: boolean;
};
export default function useStatus(params?: Params) {
@@ -16,9 +22,10 @@ export default function useStatus(params?: Params) {
const [loading, setLoading] = React.useState(
params?.initialLoading || false,
);
const [status, setStatus] = React.useState<UseStatusStatusType>({});
const [status, setStatus] = React.useState<TWUIStatusType>();
const [ready, setReady] = React.useState(params?.initialReady || false);
const [open, setOpen] = React.useState(params?.initialOpen || false);
const [error, setError] = React.useState<TWUIErrorType>();
return {
refresh,
@@ -31,5 +38,7 @@ export default function useStatus(params?: Params) {
setReady,
open,
setOpen,
error,
setError,
};
}
+2 -2
View File
@@ -1,6 +1,6 @@
import { ComponentProps, ReactNode } from "react";
import type { ComponentProps, ReactNode } from "react";
import { ArrowUpRight } from "lucide-react";
import { TWUI_LINK_LIST_LINK_OBJECT } from "../elements/LinkList";
import type { TWUI_LINK_LIST_LINK_OBJECT } from "../elements/LinkList";
import Link from "./Link";
import Row from "./Row";
+65 -26
View File
@@ -1,32 +1,50 @@
import React, {
AnchorHTMLAttributes,
ButtonHTMLAttributes,
ComponentProps,
DetailedHTMLProps,
HTMLAttributeAnchorTarget,
HTMLAttributes,
type AnchorHTMLAttributes,
type ButtonHTMLAttributes,
type ComponentProps,
type DetailedHTMLProps,
type HTMLAttributeAnchorTarget,
type HTMLAttributes,
} from "react";
import { twMerge } from "tailwind-merge";
import Loading from "../elements/Loading";
import LucideIcon, { TWUILucideIconName } from "../elements/lucide-icon";
import LucideIcon, { type TWUILucideIconName } from "../elements/lucide-icon";
import type { LucideProps } from "lucide-react";
export const TWUIButtonColors = [
"primary",
"secondary",
"text",
"white",
"accent",
"gray",
"error",
"warning",
"success",
] as const;
export const TWUIButtonSizes = [
"small",
"smaller",
"normal",
"large",
"larger",
] as const;
export const TWUIButtonVariants = ["normal", "ghost", "outlined"] as const;
export type TWUIButtonColorType = (typeof TWUIButtonColors)[number];
export type TWUIButtonSizeType = (typeof TWUIButtonSizes)[number];
export type TWUIButtonVariantType = (typeof TWUIButtonVariants)[number];
export type TWUIButtonProps = DetailedHTMLProps<
ButtonHTMLAttributes<HTMLButtonElement>,
HTMLButtonElement
> & {
title: string;
variant?: "normal" | "ghost" | "outlined";
color?:
| "primary"
| "secondary"
| "text"
| "white"
| "accent"
| "gray"
| "error"
| "warning"
| "success";
size?: "small" | "smaller" | "normal" | "large" | "larger";
variant?: TWUIButtonVariantType;
color?: TWUIButtonColorType;
size?: TWUIButtonSizeType;
loadingIconSize?: React.ComponentProps<typeof Loading>["size"];
href?: string;
target?: HTMLAttributeAnchorTarget;
@@ -37,6 +55,10 @@ export type TWUIButtonProps = DetailedHTMLProps<
>;
beforeIcon?: TWUILucideIconName | React.JSX.Element;
afterIcon?: TWUILucideIconName | React.JSX.Element;
before_icon_name?: TWUILucideIconName;
after_icon_name?: TWUILucideIconName;
before_icon_props?: LucideProps;
after_icon_props?: LucideProps;
buttonContentProps?: DetailedHTMLProps<
HTMLAttributes<HTMLDivElement>,
HTMLDivElement
@@ -44,7 +66,7 @@ export type TWUIButtonProps = DetailedHTMLProps<
loadingProps?: ComponentProps<typeof Loading>;
};
const buttonClasses: Record<string, Record<string, string>> = {
export const ButtonClasses: Record<string, Record<string, string>> = {
normal: {
primary: twMerge(
"bg-primary hover:bg-primary-hover text-white",
@@ -96,8 +118,8 @@ const buttonClasses: Record<string, Record<string, string>> = {
"twui-button-accent-outlined",
),
gray: twMerge(
"bg-transparent outline outline-1 outline-slate-300",
"text-slate-600 dark:text-white/60 dark:outline-white/30",
"bg-transparent outline outline-1 outline-gray",
"text-dark dark:text-white/60 dark:outline-white/30",
"twui-button-gray-outlined",
),
white: twMerge(
@@ -110,6 +132,11 @@ const buttonClasses: Record<string, Record<string, string>> = {
"dark:outline-error dark:text-error-dark",
"twui-button-error-outlined",
),
warning: twMerge(
"bg-transparent outline outline-1 outline-warning text-warning",
"dark:outline-warning dark:text-warning-dark",
"twui-button-warning-outlined",
),
},
ghost: {
primary: twMerge(
@@ -226,9 +253,14 @@ export default function Button({
loading,
loadingIconSize,
loadingProps,
before_icon_name,
after_icon_name,
before_icon_props,
after_icon_props,
...props
}: TWUIButtonProps) {
const finalClassName = buttonClasses[variant || "normal"]?.[color || "primary"] ?? "";
const finalClassName =
ButtonClasses[variant || "normal"]?.[color || "primary"] ?? "";
const buttonComponent = (
<button
@@ -246,7 +278,7 @@ export default function Button({
? "text-lg twui-button-large"
: size == "larger"
? "px-5 py-3 text-xl twui-button-larger"
: "twui-button-base",
: "text-sm twui-button-base",
finalClassName,
loading ? "pointer-events-none opacity-80" : "",
props.className,
@@ -262,7 +294,12 @@ export default function Button({
buttonContentProps?.className,
)}
>
{beforeIcon ? (
{before_icon_name ? (
<LucideIcon
{...before_icon_props}
name={before_icon_name}
/>
) : beforeIcon ? (
typeof beforeIcon == "string" ? (
<LucideIcon name={beforeIcon as TWUILucideIconName} />
) : (
@@ -270,7 +307,9 @@ export default function Button({
)
) : null}
{props.children}
{afterIcon ? (
{after_icon_name ? (
<LucideIcon {...after_icon_props} name={after_icon_name} />
) : afterIcon ? (
typeof afterIcon == "string" ? (
<LucideIcon name={afterIcon as TWUILucideIconName} />
) : (
+2 -2
View File
@@ -1,4 +1,4 @@
import { DetailedHTMLProps, HTMLAttributes } from "react";
import type { DetailedHTMLProps, HTMLAttributes } from "react";
import { twMerge } from "tailwind-merge";
/**
@@ -14,7 +14,7 @@ export default function Center({
className={twMerge(
"flex flex-col items-center justify-center gap-4 p-2 w-full",
"h-full twui-center",
props.className
props.className,
)}
>
{props.children}
+2 -2
View File
@@ -1,4 +1,4 @@
import { DetailedHTMLProps, HTMLAttributes } from "react";
import type { DetailedHTMLProps, HTMLAttributes } from "react";
import { twMerge } from "tailwind-merge";
/**
@@ -15,7 +15,7 @@ export default function Container({
"flex w-full max-w-container gap-4 justify-between",
"flex-wrap flex-col xl:flex-row items-start xl:items-center",
"twui-container",
props.className
props.className,
)}
>
{props.children}
+2 -2
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<
@@ -27,7 +27,7 @@ export default function Divider({ vertical, dashed, ...props }: Props) {
"twui-divider",
vertical ? "twui-divider-vertical" : "twui-divider-horizontal",
dashed ? "border-dashed" : "border-solid",
props.className
props.className,
)}
/>
);
+2 -2
View File
@@ -1,5 +1,5 @@
import { twMerge } from "tailwind-merge";
import Img, { TWUIImageProps } from "./Img";
import Img, { type TWUIImageProps } from "./Img";
import Border from "../elements/Border";
export default function DocsImg({ ...props }: TWUIImageProps) {
@@ -9,7 +9,7 @@ export default function DocsImg({ ...props }: TWUIImageProps) {
{...props}
className={twMerge(
"w-full h-auto rounded-default overflow-hidden",
props.className
props.className,
)}
/>
</Border>
+2 -2
View File
@@ -1,4 +1,4 @@
import { DetailedHTMLProps, HTMLAttributes } from "react";
import type { DetailedHTMLProps, HTMLAttributes } from "react";
import { twMerge } from "tailwind-merge";
/**
@@ -16,7 +16,7 @@ export default function Footer({
"px-4 sm:px-10 py-2",
"border-0 border-b border-slate-200 border-solid",
"twui-footer",
props.className
props.className,
)}
>
{props.children}
+2 -2
View File
@@ -1,4 +1,4 @@
import { DetailedHTMLProps, HTMLAttributes } from "react";
import type { DetailedHTMLProps, HTMLAttributes } from "react";
import { twMerge } from "tailwind-merge";
/**
@@ -15,7 +15,7 @@ export default function H1({
"text-4xl md:text-5xl mb-4",
"twui-headings twui-heading",
"twui-h1",
props.className
props.className,
)}
>
{props.children}
+2 -2
View File
@@ -1,4 +1,4 @@
import { DetailedHTMLProps, HTMLAttributes } from "react";
import type { DetailedHTMLProps, HTMLAttributes } from "react";
import { twMerge } from "tailwind-merge";
/**
@@ -15,7 +15,7 @@ export default function H2({
"text-2xl md:text-3xl mb-4",
"twui-headings twui-heading",
"twui-h2",
props.className
props.className,
)}
>
{props.children}
+2 -2
View File
@@ -1,4 +1,4 @@
import { DetailedHTMLProps, HTMLAttributes } from "react";
import type { DetailedHTMLProps, HTMLAttributes } from "react";
import { twMerge } from "tailwind-merge";
/**
@@ -15,7 +15,7 @@ export default function H3({
"text-xl mb-4",
"twui-headings twui-heading",
"twui-h3",
props.className
props.className,
)}
>
{props.children}
+2 -2
View File
@@ -1,4 +1,4 @@
import { DetailedHTMLProps, HTMLAttributes } from "react";
import type { DetailedHTMLProps, HTMLAttributes } from "react";
import { twMerge } from "tailwind-merge";
/**
@@ -15,7 +15,7 @@ export default function H4({
"text-base mb-4",
"twui-headings twui-heading",
"twui-h4",
props.className
props.className,
)}
>
{props.children}
+2 -2
View File
@@ -1,4 +1,4 @@
import { DetailedHTMLProps, HTMLAttributes } from "react";
import type { DetailedHTMLProps, HTMLAttributes } from "react";
import { twMerge } from "tailwind-merge";
/**
@@ -15,7 +15,7 @@ export default function H5({
"mb-4",
"twui-headings twui-heading",
"twui-h5",
props.className
props.className,
)}
>
{props.children}
+2 -2
View File
@@ -1,4 +1,4 @@
import { DetailedHTMLProps, HTMLAttributes } from "react";
import type { DetailedHTMLProps, HTMLAttributes } from "react";
import { twMerge } from "tailwind-merge";
/**
@@ -14,7 +14,7 @@ export default function HR({
className={twMerge(
"border-slate-200 dark:border-white/10 w-full my-4",
"twui-hr",
props.className
props.className,
)}
/>
);
+2 -2
View File
@@ -1,4 +1,4 @@
import { DetailedHTMLProps, HTMLAttributes } from "react";
import type { DetailedHTMLProps, HTMLAttributes } from "react";
import { twMerge } from "tailwind-merge";
/**
@@ -16,7 +16,7 @@ export default function Header({
"px-4 sm:px-10 py-3 flex-wrap",
"border-0 border-b border-slate-200 dark:border-white/10 border-solid",
"twui-header",
props.className
props.className,
)}
>
{props.children}
+2 -2
View File
@@ -1,5 +1,5 @@
import { ComponentProps, ReactNode } from "react";
import { TWUI_LINK_LIST_LINK_OBJECT } from "../elements/LinkList";
import type { ComponentProps, ReactNode } from "react";
import type { TWUI_LINK_LIST_LINK_OBJECT } from "../elements/LinkList";
import Link from "./Link";
import Row from "./Row";
+22 -4
View File
@@ -1,5 +1,10 @@
import _ from "lodash";
import React, { DetailedHTMLProps, ImgHTMLAttributes, ReactNode } from "react";
import React, {
type DetailedHTMLProps,
type ImgHTMLAttributes,
type ReactNode,
useEffect,
} from "react";
import { twMerge } from "tailwind-merge";
export type TWUIImageProps = DetailedHTMLProps<
@@ -18,6 +23,7 @@ export type TWUIImageProps = DetailedHTMLProps<
imgErrComp?: ReactNode;
imgErrSrcLight?: string;
imgErrSrcDark?: string;
checkImageFn?: () => Promise<boolean> | boolean;
};
/**
@@ -29,6 +35,7 @@ export default function Img({
imgErrComp,
imgErrSrcDark,
imgErrSrcLight,
checkImageFn,
...props
}: TWUIImageProps) {
const width = props.size || props.width;
@@ -58,7 +65,7 @@ export default function Img({
? "absolute top-0 left-0 w-full h-full object-cover z-0"
: "",
"twui-img",
props.className
props.className,
),
onError: (e) => {
if (props.fallbackImageSrc) {
@@ -78,6 +85,17 @@ export default function Img({
},
};
useEffect(() => {
if (checkImageFn) {
(async () => {
const does_image_exist = await checkImageFn();
if (!does_image_exist) {
setImageError(true);
}
})();
}
}, []);
if (imageError) {
return (
imgErrComp || (
@@ -101,7 +119,7 @@ export default function Img({
{...interpolatedProps}
className={twMerge(
"hidden dark:block",
interpolatedProps.className
interpolatedProps.className,
)}
src={props.srcDark}
onError={(e) => {
@@ -114,7 +132,7 @@ export default function Img({
{...interpolatedProps}
className={twMerge(
"block dark:hidden",
interpolatedProps.className
interpolatedProps.className,
)}
src={props.srcLight}
onError={(e) => {
+8 -4
View File
@@ -1,6 +1,10 @@
import { AnchorHTMLAttributes, DetailedHTMLProps, RefAttributes } from "react";
import type {
AnchorHTMLAttributes,
DetailedHTMLProps,
RefAttributes,
} from "react";
import { twMerge } from "tailwind-merge";
import { ArrowUpRight, LucideProps } from "lucide-react";
import { ArrowUpRight, type LucideProps } from "lucide-react";
type Props = DetailedHTMLProps<
AnchorHTMLAttributes<HTMLAnchorElement>,
@@ -33,7 +37,7 @@ export default function Link({
"border-0 border-b border-link dark:border-link-dark border-solid leading-4",
"twui-anchor",
"twui-a",
props.className
props.className,
)}
onClick={(e) => {
const target = e.target as HTMLElement;
@@ -52,7 +56,7 @@ export default function Link({
{...arrowProps}
className={twMerge(
"inline-block ml-1 -mt-[1px]",
arrowProps?.className
arrowProps?.className,
)}
/>
)}
+1 -1
View File
@@ -1,4 +1,4 @@
import {
import type {
AnchorHTMLAttributes,
DetailedHTMLProps,
HTMLAttributes,
+1 -1
View File
@@ -1,4 +1,4 @@
import { DetailedHTMLProps, HTMLAttributes } from "react";
import type { DetailedHTMLProps, HTMLAttributes } from "react";
import { twMerge } from "tailwind-merge";
export type LoadingRectangleBlockProps = DetailedHTMLProps<
+2 -2
View File
@@ -1,4 +1,4 @@
import { DetailedHTMLProps, HTMLAttributes } from "react";
import type { DetailedHTMLProps, HTMLAttributes } from "react";
import { twMerge } from "tailwind-merge";
/**
@@ -14,7 +14,7 @@ export default function Main({
className={twMerge(
"flex flex-col items-center w-full",
"twui-main",
props.className
props.className,
)}
>
{props.children}
+2 -2
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<
@@ -21,7 +21,7 @@ export default function P({ noMargin, ...props }: Props) {
noMargin ? "!m-0 p-0" : "",
"twui-p",
"twui-paragraph",
props.className
props.className,
)}
>
{props.children}
+2 -2
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<
@@ -22,7 +22,7 @@ export default function Row({ noWrap, itemsStart, ...props }: Props) {
noWrap ? "xl:flex-nowrap" : "flex-wrap",
itemsStart ? "items-start" : "items-center",
"twui-row",
props.className
props.className,
)}
>
{props.children}
+2 -2
View File
@@ -1,4 +1,4 @@
import { DetailedHTMLProps, HTMLAttributes } from "react";
import type { DetailedHTMLProps, HTMLAttributes } from "react";
import { twMerge } from "tailwind-merge";
/**
@@ -15,7 +15,7 @@ export default function Section({
"flex flex-col items-center w-full",
"px-4 sm:px-10 py-10",
"twui-section",
props.className
props.className,
)}
>
{props.children}
+2 -2
View File
@@ -1,5 +1,5 @@
import _ from "lodash";
import { DetailedHTMLProps, HTMLAttributes } from "react";
import type { DetailedHTMLProps, HTMLAttributes } from "react";
import { twMerge } from "tailwind-merge";
type Props = DetailedHTMLProps<
@@ -21,7 +21,7 @@ export default function Spacer({ horizontal, ...props }: Props) {
"",
horizontal ? "w-10" : "w-full h-10",
"twui-spacer",
props.className
props.className,
)}
>
{props.children}
+1 -1
View File
@@ -1,4 +1,4 @@
import { DetailedHTMLProps, HTMLAttributes } from "react";
import type { DetailedHTMLProps, HTMLAttributes } from "react";
import { twMerge } from "tailwind-merge";
/**
-37
View File
@@ -1,37 +0,0 @@
import _ from "lodash";
import { DetailedHTMLProps, HTMLAttributes } from "react";
import { twMerge } from "tailwind-merge";
type Props = DetailedHTMLProps<
HTMLAttributes<HTMLDivElement>,
HTMLDivElement
> & {
center?: boolean;
gap?: number | string;
};
/**
* # Flexbox Column
* @className twui-stack
*/
export default function Stack({ gap, ...props }: Props) {
const finalProps = _.omit(props, "center");
return (
<div
{...finalProps}
className={twMerge(
"flex flex-col items-start gap-4",
props.center && "items-center",
gap
? typeof gap == "string"
? `gap-[${gap}]`
: `gap-${gap}`
: "",
"twui-stack",
props.className
)}
>
{props.children}
</div>
);
}
+2 -2
View File
@@ -1,5 +1,5 @@
import _ from "lodash";
import { DetailedHTMLProps, HTMLAttributes } from "react";
import type { DetailedHTMLProps, HTMLAttributes } from "react";
import { twMerge } from "tailwind-merge";
type Props = DetailedHTMLProps<
@@ -29,7 +29,7 @@ export default function Stack({ gap, componentRef, ...props }: Props) {
: `gap-${gap}`
: "",
"twui-stack",
props.className
props.className,
)}
ref={componentRef}
>
+1 -3
View File
@@ -1,4 +1,4 @@
import React, { ComponentProps, useRef } from "react";
import React, { type ComponentProps, useRef } from "react";
import { twMerge } from "tailwind-merge";
import MarkdownEditorPreviewComponent from "./MarkdownEditorPreviewComponent";
import MarkdownEditorComponent from "./MarkdownEditorComponent";
@@ -73,7 +73,6 @@ export default function MarkdownEditor({
setValue={setValue}
value={value}
maxHeight={maxHeight}
refreshDepArr={[refresh]}
{...editorProps}
/>
<MarkdownEditorPreviewComponent
@@ -95,7 +94,6 @@ export default function MarkdownEditor({
setValue={setValue}
value={value}
maxHeight={maxHeight}
refreshDepArr={[refresh]}
{...editorProps}
/>
)}
@@ -1,4 +1,4 @@
import React, { ComponentProps } from "react";
import React, { type ComponentProps } from "react";
import AceEditor from "../../editors/AceEditor";
type Props = ComponentProps<typeof AceEditor> & {
@@ -1,8 +1,12 @@
import React, { ComponentProps, RefObject } from "react";
import React, { type ComponentProps } from "react";
import { twMerge } from "tailwind-merge";
// @ts-ignore
import { serialize } from "next-mdx-remote/serialize";
// @ts-ignore
import remarkGfm from "remark-gfm";
// @ts-ignore
import rehypePrismPlus from "rehype-prism-plus";
// @ts-ignore
import { MDXRemote, MDXRemoteSerializeResult } from "next-mdx-remote";
import Border from "../../elements/Border";
import { useMDXComponents } from "../mdx-components";
@@ -45,9 +49,11 @@ export default function MarkdownEditorPreviewComponent({
},
// scope: data,
})
// @ts-ignore
.then((mdxSrc) => {
setMdxSource(mdxSrc);
})
// @ts-ignore
.catch((err) => {});
} catch (error) {}
}, [value]);
@@ -58,7 +64,7 @@ export default function MarkdownEditorPreviewComponent({
className={twMerge(
`w-full max-h-[${maxHeight}] h-[${maxHeight}] block px-6 pb-10`,
"overflow-auto",
wrapperProps?.className
wrapperProps?.className,
)}
>
{mdxSource ? (
+1 -1
View File
@@ -1,4 +1,4 @@
import { MDXComponents } from "mdx/types";
import { type MDXComponents } from "mdx/types";
import CodeBlock from "../elements/CodeBlock";
import H1 from "../layout/H1";
import H4 from "../layout/H4";
+73
View File
@@ -1,3 +1,5 @@
import type { Ace } from "ace-builds";
export const AutocompleteOptions = [
// Personal Information
"name",
@@ -66,3 +68,74 @@ export const AutocompleteOptions = [
"on", // Enables autofill (default)
"off", // Disables autofill
] as const;
export interface AceEditorOptions {
animatedScroll?: boolean;
autoScrollEditorIntoView?: boolean;
behavioursEnabled?: boolean;
copyWithEmptySelection?: boolean;
cursorStyle?: "ace" | "slim" | "smooth" | "wide";
customScrollbar?: boolean;
displayIndentGuides?: boolean;
dragDelay?: number;
dragEnabled?: boolean;
enableAutoIndent?: boolean;
enableBasicAutocompletion?: boolean | any[];
enableKeyboardAccessibility?: boolean;
enableLiveAutocompletion?: boolean | any[];
enableMobileMenu?: boolean;
enableMultiselect?: boolean;
enableSnippets?: boolean;
fadeFoldWidgets?: boolean;
firstLineNumber?: number;
fixedWidthGutter?: boolean;
focusTimeout?: number;
foldStyle?: "markbegin" | "markbeginend" | "manual";
fontFamily?: string;
fontSize?: number;
hScrollBarAlwaysVisible?: boolean;
hasCssTransforms?: boolean;
highlightActiveLine?: boolean;
highlightGutterLine?: boolean;
highlightIndentGuides?: boolean;
highlightSelectedWord?: boolean;
indentedSoftWrap?: boolean;
keyboardHandler?: string;
liveAutocompletionDelay?: number;
liveAutocompletionThreshold?: number;
maxLines?: number;
maxPixelHeight?: number;
mergeUndoDeltas?: boolean | "always";
minLines?: number;
mode?: string;
navigateWithinSoftTabs?: boolean;
newLineMode?: Ace.NewLineMode;
overwrite?: boolean;
placeholder?: string;
printMargin?: number | boolean;
printMarginColumn?: number;
readOnly?: boolean;
relativeLineNumbers?: boolean;
scrollPastEnd?: number;
scrollSpeed?: number;
selectionStyle?: string;
session?: any;
showFoldWidgets?: boolean;
showFoldedAnnotations?: boolean;
showGutter?: boolean;
showInvisibles?: boolean;
showLineNumbers?: boolean;
showPrintMargin?: boolean;
tabSize?: number;
textInputAriaLabel?: string;
theme?: string;
tooltipFollowsMouse?: boolean;
useSoftTabs?: boolean;
useSvgGutterIcons?: boolean;
useWorker?: boolean;
vScrollBarAlwaysVisible?: boolean;
value?: string;
wrap?: number | boolean | "off" | "free" | "printmargin";
wrapBehavioursEnabled?: boolean;
wrapMethod?: "code" | "text" | "auto";
}
@@ -0,0 +1,44 @@
import { describe, expect, test } from "bun:test";
import twuiCamelToNormalCase from "./camel-to-normal-case";
describe("twuiCamelToNormalCase", () => {
test("converts simple camelCase", () => {
expect(twuiCamelToNormalCase("camelCase")).toBe("Camel Case");
});
test("converts pascalCase", () => {
expect(twuiCamelToNormalCase("PascalCase")).toBe("Pascal Case");
});
test("handles single lowercase word", () => {
expect(twuiCamelToNormalCase("word")).toBe("Word");
});
test("handles single uppercase word", () => {
expect(twuiCamelToNormalCase("Word")).toBe("Word");
});
test("handles multiple uppercase transitions", () => {
expect(twuiCamelToNormalCase("firstNameLastName")).toBe("First Name Last Name");
});
test("handles consecutive uppercase letters", () => {
expect(twuiCamelToNormalCase("XMLParser")).toBe("X M L Parser");
});
test("handles already normal text", () => {
expect(twuiCamelToNormalCase("Normal")).toBe("Normal");
});
test("handles empty string", () => {
expect(twuiCamelToNormalCase("")).toBe("");
});
test("handles string with numbers", () => {
expect(twuiCamelToNormalCase("field123")).toBe("Field123");
});
test("handles acronym at end", () => {
expect(twuiCamelToNormalCase("userID")).toBe("User I D");
});
});
+97
View File
@@ -0,0 +1,97 @@
import { describe, expect, test } from "bun:test";
import TWUIEJSON from "./ejson";
describe("TWUIEJSON.parse", () => {
test("parses valid JSON string", () => {
expect(TWUIEJSON.parse('{"key":"value"}')).toEqual({ key: "value" });
});
test("parses valid JSON array string", () => {
expect(TWUIEJSON.parse("[1,2,3]")).toEqual([1, 2, 3]);
});
test("returns undefined for null input", () => {
expect(TWUIEJSON.parse(null)).toBeUndefined();
});
test("returns undefined for undefined input (0 is falsy)", () => {
expect(TWUIEJSON.parse(0)).toBeUndefined();
});
test("returns undefined for number input", () => {
expect(TWUIEJSON.parse(42)).toBeUndefined();
});
test("returns undefined for empty string", () => {
expect(TWUIEJSON.parse("")).toBeUndefined();
});
test("returns object as-is for object input", () => {
const obj = { a: 1 };
expect(TWUIEJSON.parse(obj as any)).toBe(obj);
});
test("returns undefined for invalid JSON string", () => {
expect(TWUIEJSON.parse("not json")).toBeUndefined();
});
test("returns undefined for malformed JSON", () => {
expect(TWUIEJSON.parse("{invalid}")).toBeUndefined();
});
test("parses nested JSON", () => {
const result = TWUIEJSON.parse('{"outer":{"inner":42}}');
expect(result).toEqual({ outer: { inner: 42 } });
});
test("supports reviver function", () => {
const result = TWUIEJSON.parse('{"a":"1","b":"2"}', (_key, value) =>
typeof value === "string" && !isNaN(Number(value)) ? Number(value) : value
);
expect(result).toEqual({ a: 1, b: 2 });
});
test("parses boolean JSON values", () => {
expect(TWUIEJSON.parse("true") as unknown).toBe(true);
expect(TWUIEJSON.parse("false") as unknown).toBe(false);
});
test("parses null JSON value", () => {
expect(TWUIEJSON.parse("null") as unknown).toBeNull();
});
});
describe("TWUIEJSON.stringify", () => {
test("stringifies an object", () => {
expect(TWUIEJSON.stringify({ key: "value" })).toBe('{"key":"value"}');
});
test("stringifies an array", () => {
expect(TWUIEJSON.stringify([1, 2, 3])).toBe("[1,2,3]");
});
test("returns undefined for circular reference", () => {
const obj: any = { a: 1 };
obj.self = obj;
expect(TWUIEJSON.stringify(obj)).toBeUndefined();
});
test("supports replacer function", () => {
const result = TWUIEJSON.stringify({ a: 1, b: 2 }, (_key, value) =>
typeof value === "number" ? value * 2 : value
);
expect(result).toBe('{"a":2,"b":4}');
});
test("supports space parameter for formatting", () => {
const result = TWUIEJSON.stringify({ a: 1 }, null, 2);
expect(result).toContain('\n');
expect(result).toContain(' ');
});
test("stringifies primitive values", () => {
expect(TWUIEJSON.stringify("hello")).toBe('"hello"');
expect(TWUIEJSON.stringify(42)).toBe("42");
expect(TWUIEJSON.stringify(null)).toBe("null");
});
});
+4 -5
View File
@@ -27,8 +27,6 @@ export default async function imageInputToBase64({
let imageName = finalFile.name.replace(/\..*/, "");
let imageDataBase64: string | undefined;
const MIME_TYPE = mimeType ? mimeType : finalFile.type;
const QUALITY = 0.95;
const MAX_WIDTH = maxWidth ? maxWidth : null;
@@ -38,10 +36,11 @@ export default async function imageInputToBase64({
img.src = blobURL;
imageDataBase64 = await new Promise((res, rej) => {
const imageDataBase64 = await new Promise<string>((res, rej) => {
img.onerror = function () {
URL.revokeObjectURL(this.src);
window.alert("Cannot load image!");
rej(new Error("Cannot load image"));
};
img.onload = function () {
@@ -72,7 +71,7 @@ export default async function imageInputToBase64({
res(srcEncoded);
};
});
}).catch(() => undefined);
return {
imageBase64: imageDataBase64?.replace(/.*?base64,/, ""),
@@ -89,4 +88,4 @@ export default async function imageInputToBase64({
imageName: undefined,
};
}
}
}
@@ -0,0 +1,42 @@
import { describe, expect, test } from "bun:test";
import lowerToTitleCase from "./lower-to-title-case";
describe("lowerToTitleCase", () => {
test("converts snake_case to Title Case", () => {
expect(lowerToTitleCase("hello_world")).toBe("Hello World");
});
test("converts kebab-case to Title Case", () => {
expect(lowerToTitleCase("hello-world")).toBe("Hello World");
});
test("converts mixed snake-kebab", () => {
expect(lowerToTitleCase("hello_world-test")).toBe("Hello World Test");
});
test("handles single word", () => {
expect(lowerToTitleCase("hello")).toBe("Hello");
});
test("handles already title case", () => {
expect(lowerToTitleCase("Hello")).toBe("Hello");
});
test("handles empty string", () => {
expect(lowerToTitleCase("")).toBe("");
});
test("handles all uppercase input", () => {
expect(lowerToTitleCase("HELLO_WORLD")).toBe("Hello World");
});
test("handles mixed case input", () => {
expect(lowerToTitleCase("hello_World")).toBe("Hello World");
});
test("converts user type values correctly", () => {
expect(lowerToTitleCase("super_admin")).toBe("Super Admin");
expect(lowerToTitleCase("financial_secretary")).toBe("Financial Secretary");
expect(lowerToTitleCase("primary_resident")).toBe("Primary Resident");
});
});
+50
View File
@@ -0,0 +1,50 @@
import { describe, expect, test } from "bun:test";
import twuiNormalizeText from "./normalize-text";
describe("twuiNormalizeText", () => {
test("collapses multiple spaces", () => {
expect(twuiNormalizeText("hello world")).toBe("hello world");
});
test("replaces newlines with spaces", () => {
expect(twuiNormalizeText("hello\nworld")).toBe("hello world");
});
test("replaces carriage returns with spaces", () => {
expect(twuiNormalizeText("hello\rworld")).toBe("hello world");
});
test("replaces CRLF with spaces", () => {
expect(twuiNormalizeText("hello\r\nworld")).toBe("hello world");
});
test("trims leading and trailing whitespace", () => {
expect(twuiNormalizeText(" hello world ")).toBe("hello world");
});
test("handles mixed whitespace", () => {
expect(twuiNormalizeText(" hello\n\n world \r\n ")).toBe("hello world");
});
test("handles empty string", () => {
expect(twuiNormalizeText("")).toBe("");
});
test("handles string with only whitespace", () => {
expect(twuiNormalizeText(" ")).toBe("");
});
test("preserves single spaces", () => {
expect(twuiNormalizeText("hello world")).toBe("hello world");
});
test("handles tabs as regular characters (not collapsed by default)", () => {
const result = twuiNormalizeText("hello\tworld");
expect(result).toContain("hello");
expect(result).toContain("world");
});
test("handles multiple consecutive newlines", () => {
expect(twuiNormalizeText("hello\n\n\nworld")).toBe("hello world");
});
});
+85
View File
@@ -0,0 +1,85 @@
import { describe, expect, test } from "bun:test";
import twuiNumberfy from "./numberfy";
describe("twuiNumberfy", () => {
test("converts a number to number", () => {
expect(twuiNumberfy(42)).toBe(42);
});
test("converts a numeric string to number", () => {
expect(twuiNumberfy("42")).toBe(42);
});
test("strips non-numeric characters", () => {
expect(twuiNumberfy("$1,234.56")).toBe(1234.56);
});
test("returns 0 for non-numeric string", () => {
expect(twuiNumberfy("hello")).toBe(0);
});
test("returns 0 for undefined input", () => {
expect(twuiNumberfy(undefined)).toBe(0);
});
test("returns 0 for null input", () => {
expect(twuiNumberfy(null)).toBe(0);
});
test("rounds with decimals=0", () => {
expect(twuiNumberfy(3.7, 0)).toBe(4);
});
test("rounds with decimals=0 (floor case)", () => {
expect(twuiNumberfy(3.2, 0)).toBe(3);
});
test("limits decimal places", () => {
expect(twuiNumberfy(3.14159, 2)).toBe(3.14);
});
test("preserves existing decimals when no decimals param", () => {
expect(twuiNumberfy("3.14")).toBe(3.14);
});
test("rounds integers when no decimals param and no existing decimals", () => {
expect(twuiNumberfy("42")).toBe(42);
});
test("strips minus sign (does not support negatives)", () => {
expect(twuiNumberfy("-5")).toBe(5);
});
test("handles string with currency symbol", () => {
expect(twuiNumberfy("₦50,000")).toBe(50000);
});
test("handles empty string", () => {
expect(twuiNumberfy("")).toBe(0);
});
test("handles boolean true", () => {
expect(twuiNumberfy(true)).toBe(0);
});
test("handles boolean false", () => {
expect(twuiNumberfy(false)).toBe(0);
});
test("handles string with percentage", () => {
expect(twuiNumberfy("75%")).toBe(75);
});
test("handles multiple decimal points (results in NaN -> 0)", () => {
const result = twuiNumberfy("1.2.3");
expect(result).toBe(0);
});
test("handles zero", () => {
expect(twuiNumberfy(0)).toBe(0);
});
test("handles very large numbers", () => {
expect(twuiNumberfy("9999999999")).toBe(9999999999);
});
});

Some files were not shown because too many files have changed in this diff Show More