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 ReactDOM from "react-dom";
import Button from "../layout/Button"; import Button from "../layout/Button";
import { X } from "lucide-react"; 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 Paper from "../elements/Paper";
import _ from "lodash"; import _ from "lodash";
@@ -15,15 +15,20 @@ type Props = TWUI_MODAL_PROPS & {
/** /**
* # Modal Main Component * # 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; if (!open) return null;
return ReactDOM.createPortal( return ReactDOM.createPortal(
<div <div
className={twMerge( 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", "flex flex-col items-center justify-center p-4",
"twui-modal-root" "twui-modal-root",
)} )}
role="dialog" role="dialog"
aria-modal="true" aria-modal="true"
@@ -31,7 +36,7 @@ export default function ModalComponent({ open, setOpen, ...props }: Props) {
<div <div
className={twMerge( className={twMerge(
"absolute top-0 left-0 bg-dark/80 z-0", "absolute top-0 left-0 bg-dark/80 z-0",
"w-screen h-screen" "w-screen h-screen",
)} )}
onClick={(e) => { onClick={(e) => {
setOpen(false); 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", "z-10 max-w-modal bg-background-light dark:bg-background-dark",
"w-full relative max-h-[95vh] overflow-y-auto", "w-full relative max-h-[95vh] overflow-y-auto",
"twui-modal-content", "twui-modal-content",
props.className props.className,
)} )}
> >
{props.children} {props.children}
<Button {!no_cancel_button && (
className="absolute top-0 right-0 p-2" <Button
variant="ghost" className="absolute top-0 right-0 p-2"
color="gray" variant="ghost"
onClick={() => { color="gray"
setOpen(false); onClick={() => {
}} setOpen(false);
title="Close Modal Button" }}
> title="Close Modal Button"
<X size={30} /> >
</Button> <X size={30} />
</Button>
)}
</Paper> </Paper>
</div>, </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 React from "react";
import { twMerge } from "tailwind-merge"; import { twMerge } from "tailwind-merge";
import ReactDOM from "react-dom"; 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 Paper from "../elements/Paper";
import _ from "lodash"; import _ from "lodash";
import twuiGrabPopoverStyles from "../(functions)/popver/grab-popover-styles"; import twuiGrabPopoverStyles from "../(functions)/popver/grab-popover-styles";
@@ -44,17 +44,18 @@ export default function PopoverComponent({
} }
}, [open, targetElRef, position]); }, [open, targetElRef, position]);
let closeTimeout: any; const closeTimeoutRef = React.useRef<any>(null);
const popoverEnterFn = React.useCallback(() => { const popoverEnterFn = React.useCallback(() => {
popoverContentActiveRef.current = true; popoverContentActiveRef.current = true;
popoverTargetActiveRef.current = false; popoverTargetActiveRef.current = false;
window.clearTimeout(closeTimeoutRef.current);
setOpen(true); setOpen(true);
}, []); }, []);
const popoverLeaveFn = React.useCallback(() => { const popoverLeaveFn = React.useCallback(() => {
window.clearTimeout(closeTimeout); window.clearTimeout(closeTimeoutRef.current);
closeTimeout = setTimeout(() => { closeTimeoutRef.current = setTimeout(() => {
if (popoverTargetActiveRef.current) { if (popoverTargetActiveRef.current) {
popoverTargetActiveRef.current = false; popoverTargetActiveRef.current = false;
return; return;
@@ -71,7 +72,7 @@ export default function PopoverComponent({
className={twMerge( className={twMerge(
"max-w-[300px] z-[250]", "max-w-[300px] z-[250]",
"twui-popover-content", "twui-popover-content",
props.className props.className,
)} )}
style={{ ...style, ...props.style }} style={{ ...style, ...props.style }}
onMouseEnter={ onMouseEnter={
@@ -110,6 +111,6 @@ export default function PopoverComponent({
/> */} /> */}
{props.children} {props.children}
</Paper>, </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 ### Install packages
```sh ```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 ```sh
+9 -8
View File
@@ -21,14 +21,14 @@
--color-primary-dark-outline: #4b4b4b; --color-primary-dark-outline: #4b4b4b;
--color-primary-dark-text: #ffffff; --color-primary-dark-text: #ffffff;
--color-secondary: #000000; --color-secondary: #2b4bf0;
--color-secondary-hover: #dddddd; --color-secondary-hover: #2b4bf0;
--color-secondary-outline: #dddddd; --color-secondary-outline: #2b4bf0;
--color-secondary-text: #dddddd; --color-secondary-text: #2b4bf0;
--color-secondary-dark: #000000; --color-secondary-dark: #2b4bf0;
--color-secondary-dark-hover: #dddddd; --color-secondary-dark-hover: #2b4bf0;
--color-secondary-dark-outline: #dddddd; --color-secondary-dark-outline: #2b4bf0;
--color-secondary-dark-text: #dddddd; --color-secondary-dark-text: #2b4bf0;
--color-accent: #000000; --color-accent: #000000;
--color-accent-hover: #dddddd; --color-accent-hover: #dddddd;
@@ -51,6 +51,7 @@
--color-error-dark: #e5484d; --color-error-dark: #e5484d;
--color-warning: #ff6900; --color-warning: #ff6900;
--color-warning-dark: #ff8c3a;
--color-link: #0051c9; --color-link: #0051c9;
--color-link-dark: #548adb; --color-link-dark: #548adb;
+2 -2
View File
@@ -1,5 +1,5 @@
import { DetailedHTMLProps, HTMLAttributes } from "react"; import type { DetailedHTMLProps, HTMLAttributes } from "react";
import { DocsLinkType } from "."; import type { DocsLinkType } from ".";
import Stack from "../../layout/Stack"; import Stack from "../../layout/Stack";
import TWUIDocsLink from "./TWUIDocsLink"; import TWUIDocsLink from "./TWUIDocsLink";
import { twMerge } from "tailwind-merge"; import { twMerge } from "tailwind-merge";
+9 -9
View File
@@ -1,9 +1,9 @@
import React, { import React, {
AnchorHTMLAttributes, type AnchorHTMLAttributes,
ComponentProps, type ComponentProps,
DetailedHTMLProps, type DetailedHTMLProps,
} from "react"; } from "react";
import { DocsLinkType } from "."; import type { DocsLinkType } from ".";
import Stack from "../../layout/Stack"; import Stack from "../../layout/Stack";
import { twMerge } from "tailwind-merge"; import { twMerge } from "tailwind-merge";
import Row from "../../layout/Row"; import Row from "../../layout/Row";
@@ -45,11 +45,11 @@ export default function TWUIDocsLink({
React.useEffect(() => { React.useEffect(() => {
if (typeof window !== "undefined") { if (typeof window !== "undefined") {
const basePathMatch = window.location.pathname.includes( const basePathMatch = window.location.pathname.includes(
docLink.href docLink.href,
); );
const isStrictMatch = Boolean( const isStrictMatch = Boolean(
linkRef.current?.getAttribute("data-strict") linkRef.current?.getAttribute("data-strict"),
); );
if (strict || isStrictMatch) { if (strict || isStrictMatch) {
@@ -79,7 +79,7 @@ export default function TWUIDocsLink({
"twui-docs-left-aside-link whitespace-nowrap", "twui-docs-left-aside-link whitespace-nowrap",
"grow overflow-hidden overflow-ellipsis", "grow overflow-hidden overflow-ellipsis",
isActive ? "active" : "", isActive ? "active" : "",
props.className props.className,
)} )}
ref={linkRef} ref={linkRef}
data-strict={strict || docLink.strict} data-strict={strict || docLink.strict}
@@ -93,7 +93,7 @@ export default function TWUIDocsLink({
color="gray" color="gray"
className={twMerge( className={twMerge(
"p-1 hover:opacity-100", "p-1 hover:opacity-100",
expand ? "rotate-180 opacity-30" : "opacity-70" expand ? "rotate-180 opacity-30" : "opacity-70",
)} )}
onClick={() => setExpand(!expand)} onClick={() => setExpand(!expand)}
title="Docs Aside Links Dropdown Button" title="Docs Aside Links Dropdown Button"
@@ -107,7 +107,7 @@ export default function TWUIDocsLink({
<Stack <Stack
className={twMerge( className={twMerge(
"gap-2 w-full pl-3", "gap-2 w-full pl-3",
childWrapperProps?.className childWrapperProps?.className,
)} )}
{...childWrapperProps} {...childWrapperProps}
> >
@@ -1,5 +1,5 @@
import React, { DetailedHTMLProps, HTMLAttributes } from "react"; import React, { type DetailedHTMLProps, type HTMLAttributes } from "react";
import { DocsLinkType } from "."; import type { DocsLinkType } from ".";
import Stack from "../../layout/Stack"; import Stack from "../../layout/Stack";
import TWUIDocsLink from "./TWUIDocsLink"; import TWUIDocsLink from "./TWUIDocsLink";
import { twMerge } from "tailwind-merge"; import { twMerge } from "tailwind-merge";
@@ -28,7 +28,7 @@ export default function TWUIDocsRightAside({
if (!ready) return; if (!ready) return;
const headerHrefs = document.querySelectorAll( const headerHrefs = document.querySelectorAll(
".twui-docs-header-anchor" ".twui-docs-header-anchor",
); );
const linksArr: DocsLinkType[] = []; const linksArr: DocsLinkType[] = [];
@@ -87,7 +87,7 @@ export default function TWUIDocsRightAside({
if (!links.length) return; if (!links.length) return;
const headerHrefs = document.querySelectorAll( const headerHrefs = document.querySelectorAll(
"a.twui-docs-header-anchor" "a.twui-docs-header-anchor",
); );
const observer = new IntersectionObserver((entries) => { const observer = new IntersectionObserver((entries) => {
@@ -96,7 +96,7 @@ export default function TWUIDocsRightAside({
const id = entry.target.id; const id = entry.target.id;
const link = document.querySelector( const link = document.querySelector(
`.twui-docs-right-aside a[href="#${id}"]` `.twui-docs-right-aside a[href="#${id}"]`,
); );
if (link) { if (link) {
link.classList.add("active"); link.classList.add("active");
@@ -104,7 +104,7 @@ export default function TWUIDocsRightAside({
} else { } else {
const id = entry.target.id; const id = entry.target.id;
const link = document.querySelector( const link = document.querySelector(
`.twui-docs-right-aside a[href="#${id}"]` `.twui-docs-right-aside a[href="#${id}"]`,
); );
if (link) { if (link) {
link.classList.remove("active"); link.classList.remove("active");
@@ -130,7 +130,7 @@ export default function TWUIDocsRightAside({
className={twMerge( className={twMerge(
"pb-10 hidden xl:flex min-w-[150px] max-w-[200px]", "pb-10 hidden xl:flex min-w-[150px] max-w-[200px]",
"sticky top-6 twui-docs-right-aside", "sticky top-6 twui-docs-right-aside",
props.className props.className,
)} )}
> >
<Stack className="w-full overflow-hidden"> <Stack className="w-full overflow-hidden">
+3 -3
View File
@@ -1,4 +1,4 @@
import { import type {
ComponentProps, ComponentProps,
DetailedHTMLProps, DetailedHTMLProps,
HTMLAttributes, HTMLAttributes,
@@ -61,7 +61,7 @@ export default function TWUIDocs({
{...docsContentProps} {...docsContentProps}
className={twMerge( className={twMerge(
"items-start gap-8 w-full flex-nowrap", "items-start gap-8 w-full flex-nowrap",
docsContentProps?.className docsContentProps?.className,
)} )}
> >
<TWUIDocsAside <TWUIDocsAside
@@ -74,7 +74,7 @@ export default function TWUIDocs({
<div <div
className={twMerge( className={twMerge(
"block twui-docs-content pl-0 xl:pl-6 grow", "block twui-docs-content pl-0 xl:pl-6 grow",
"overflow-hidden" "overflow-hidden",
)} )}
> >
{children} {children}
+176 -172
View File
@@ -1,172 +1,176 @@
import React, { MutableRefObject } from "react"; import React, { type RefObject } from "react";
import { twMerge } from "tailwind-merge"; import { twMerge } from "tailwind-merge";
import AceEditorModes from "./ace-editor-modes"; import AceEditorModes from "./ace-editor-modes";
import { AceEditorOptions } from "@moduletrace/datasquirel/dist/package-shared/types"; import type { Ace as AceAjax } from "ace-builds";
export type AceEditorComponentType = { export type AceEditorComponentType = {
editorRef?: MutableRefObject<AceAjax.Editor | undefined>; editorRef?: RefObject<AceAjax.Editor | undefined>;
readOnly?: boolean; readOnly?: boolean;
/** Function to call when Ctrl+Enter is pressed */ /** Function to call when Ctrl+Enter is pressed */
ctrlEnterFn?: (editor: AceAjax.Editor) => void; ctrlEnterFn?: (editor: AceAjax.Editor) => void;
content?: string; content?: string;
placeholder?: string; placeholder?: string;
title?: string; title?: string;
mode?: (typeof AceEditorModes)[number]; mode?: (typeof AceEditorModes)[number];
fontSize?: string; fontSize?: string;
previewMode?: boolean; previewMode?: boolean;
onChange?: (value: string) => void; onChange?: (value: string, editor?: AceAjax.Editor) => void;
delay?: number; delay?: number;
wrapperProps?: React.DetailedHTMLProps< wrapperProps?: React.DetailedHTMLProps<
React.HTMLAttributes<HTMLDivElement>, React.HTMLAttributes<HTMLDivElement>,
HTMLDivElement HTMLDivElement
>; >;
refreshDepArr?: any[]; editorOptions?: AceAjax.EditorOptions;
editorOptions?: AceEditorOptions; showLabel?: boolean;
showLabel?: boolean; };
};
let timeout: any;
let timeout: any;
/**
/** * # Powerful Ace Editor
* # Powerful Ace Editor * @note **NOTE** head scripts required
* @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/ace.min.js` * @script `https://cdnjs.cloudflare.com/ajax/libs/ace/1.22.0/ext-language_tools.min.js`
* @script `https://cdnjs.cloudflare.com/ajax/libs/ace/1.22.0/ext-language_tools.min.js` */
*/ export default function AceEditor({
export default function AceEditor({ editorRef,
editorRef, readOnly,
readOnly, ctrlEnterFn,
ctrlEnterFn, content = "",
content = "", placeholder,
placeholder, mode,
mode, fontSize,
fontSize, previewMode,
previewMode, onChange,
onChange, delay = 500,
delay = 500, wrapperProps,
refreshDepArr, editorOptions,
wrapperProps, showLabel,
editorOptions, title,
showLabel, }: AceEditorComponentType) {
title, try {
}: AceEditorComponentType) { const editorElementRef = React.useRef<HTMLDivElement>(undefined);
try { const editorRefInstance = React.useRef<AceAjax.Editor>(undefined);
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 [refresh, setRefresh] = React.useState(0); const [ready, setReady] = React.useState(false);
const [darkMode, setDarkMode] = React.useState(false);
const [ready, setReady] = React.useState(false); React.useEffect(() => {
if (!ready) return;
React.useEffect(() => {
if (!ready) return; // @ts-ignore
const ace = window.ace;
if (!ace?.edit || !editorElementRef.current) {
setTimeout(() => { if (!ace?.edit || !editorElementRef.current) {
setRefresh((prev) => prev + 1); setTimeout(() => {
}, 1000); setRefresh((prev) => prev + 1);
return; }, 1000);
} return;
}
const editor = ace.edit(editorElementRef.current);
// @ts-ignore
editor.setOptions({ const editor = ace.edit(editorElementRef.current) as AceAjax.Editor;
mode: `ace/mode/${mode ? mode : "javascript"}`,
theme: darkMode editor.setOptions({
? "ace/theme/tomorrow_night_eighties" mode: `ace/mode/${mode ? mode : "javascript"}`,
: "ace/theme/ace_light", theme: darkMode
value: (() => { ? "ace/theme/tomorrow_night_eighties"
try { : "ace/theme/ace_light",
return JSON.stringify(JSON.parse(content), null, 4); value: (() => {
} catch (error) { try {
return content; return JSON.stringify(JSON.parse(content), null, 4);
} } catch (error) {
})(), return content;
placeholder: placeholder ? placeholder : "", }
enableBasicAutocompletion: true, })(),
enableLiveAutocompletion: true, placeholder: placeholder ? placeholder : "",
readOnly: readOnly ? true : false, enableBasicAutocompletion: true,
fontSize: fontSize ? fontSize : null, enableLiveAutocompletion: true,
showLineNumbers: previewMode ? false : true, readOnly: readOnly ? true : false,
wrap: true, fontSize: fontSize ? fontSize : undefined,
wrapMethod: "code", showLineNumbers: previewMode ? false : true,
...editorOptions, wrap: true,
}); wrapMethod: "code",
...editorOptions,
editor.commands.addCommand({ });
name: "myCommand",
bindKey: { win: "Ctrl-Enter", mac: "Command-Enter" }, editor.commands.addCommand({
exec: function (editor) { name: "myCommand",
if (ctrlEnterFn) ctrlEnterFn(editor); bindKey: { win: "Ctrl-Enter", mac: "Command-Enter" },
}, exec: function (editor) {
readOnly: true, if (ctrlEnterFn) ctrlEnterFn(editor);
}); },
readOnly: true,
editor.getSession().on("change", function (e) { });
if (onChange) {
clearTimeout(timeout); editor.getSession().on("change", function (e) {
if (onChange) {
setTimeout(() => { clearTimeout(timeout);
try {
onChange(editor.getValue()); setTimeout(() => {
} catch (error) {} try {
}, delay); onChange(
} editor.getValue(),
}); editorRefInstance.current,
);
editorRefInstance.current = editor; } catch (error) {}
if (editorRef) editorRef.current = editor; }, delay);
}
return function () { });
editor.destroy();
}; editorRefInstance.current = editor;
}, [refresh, darkMode, ready, mode, ...(refreshDepArr || [])]); if (editorRef) editorRef.current = editor;
React.useEffect(() => { return function () {
const htmlClassName = document.documentElement.className; editor.destroy();
if (htmlClassName.match(/dark/i)) setDarkMode(true); };
setTimeout(() => { }, [refresh, darkMode, ready, mode]);
setReady(true);
}, 200); React.useEffect(() => {
}, []); const htmlClassName = document.documentElement.className;
if (htmlClassName.match(/dark/i)) setDarkMode(true);
return ( setTimeout(() => {
<React.Fragment> setReady(true);
<div }, 200);
{...wrapperProps} }, []);
className={twMerge(
"w-full h-[400px] block rounded-default", return (
"border border-slate-200 border-solid relative", <React.Fragment>
"dark:border-white/20", <div
showLabel && title ? "pt-4" : "", {...wrapperProps}
wrapperProps?.className, className={twMerge(
)} "w-full h-[400px] block rounded-default",
> "border border-border border-solid relative",
{showLabel && title ? ( "dark:border-white/20",
<label showLabel && title ? "pt-4" : "",
className={twMerge( wrapperProps?.className,
"bg-background-light dark:bg-background-dark text-xs", )}
"-top-3 left-2 px-2 py-1 absolute z-10", >
)} {showLabel && title ? (
> <label
{title} className={twMerge(
</label> "bg-background-light dark:bg-background-dark text-xs",
) : null} "-top-3 left-2 px-2 py-1 absolute z-10",
<div )}
ref={editorElementRef as any} >
className="w-full h-full" {title}
></div> </label>
</div> ) : null}
</React.Fragment> <div
); ref={editorElementRef as any}
} catch (error: any) { className="w-full h-full"
return ( ></div>
<React.Fragment> </div>
<span className="m-0"> </React.Fragment>
Editor Error:{" "} );
<b className="text-red-600">{error.message}</b> } catch (error: any) {
</span> return (
</React.Fragment> <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 React, { useCallback, type ComponentProps, type RefObject } from "react";
import { RawEditorOptions, TinyMCE, Editor } from "./tinymce"; import { type Editor, type RawEditorOptions } from "./tinymce";
import { twMerge } from "tailwind-merge"; import { twMerge } from "tailwind-merge";
import twuiSlugToNormalText from "../../utils/slug-to-normal-text"; import twuiSlugToNormalText from "../../utils/slug-to-normal-text";
import Border from "../../elements/Border"; import Border from "../../elements/Border";
@@ -7,7 +7,7 @@ import useTinyMCE from "./useTinyMCE";
export type TinyMCEEditorProps<KeyType extends string> = { export type TinyMCEEditorProps<KeyType extends string> = {
options?: RawEditorOptions; options?: RawEditorOptions;
editorRef?: React.MutableRefObject<Editor | null>; editorRef?: React.RefObject<Editor | null>;
setEditor?: React.Dispatch<React.SetStateAction<Editor>>; setEditor?: React.Dispatch<React.SetStateAction<Editor>>;
wrapperProps?: React.DetailedHTMLProps< wrapperProps?: React.DetailedHTMLProps<
React.HTMLAttributes<HTMLDivElement>, React.HTMLAttributes<HTMLDivElement>,
@@ -20,11 +20,12 @@ export type TinyMCEEditorProps<KeyType extends string> = {
borderProps?: ComponentProps<typeof Border>; borderProps?: ComponentProps<typeof Border>;
defaultValue?: string; defaultValue?: string;
name?: KeyType; name?: KeyType;
changeHandler?: (content: string) => void; changeHandler?: (content: string, editor?: Editor) => void;
showLabel?: boolean; showLabel?: boolean;
useParentCSS?: boolean; useParentCSS?: boolean;
placeholder?: string; placeholder?: string;
refreshDependencyArray?: any[]; refreshDependencyArray?: any[];
base_url?: string;
}; };
/** /**
@@ -45,11 +46,12 @@ export default function TinyMCEEditor<KeyType extends string>({
useParentCSS, useParentCSS,
placeholder, placeholder,
refreshDependencyArray, refreshDependencyArray,
base_url,
}: TinyMCEEditorProps<KeyType>) { }: TinyMCEEditorProps<KeyType>) {
const { tinyMCE } = useTinyMCE(); const { tinyMCE } = useTinyMCE({ base_url });
const editorComponentRef = React.useRef<HTMLDivElement>(null); const editorComponentRef = React.useRef<HTMLDivElement>(null);
const editorRef: MutableRefObject<Editor | null> = const editorRef: RefObject<Editor | null> =
passedEditorRef || React.useRef(null); passedEditorRef || React.useRef(null);
const EDITOR_VALUE_CHANGE_TIMEOUT = 500; const EDITOR_VALUE_CHANGE_TIMEOUT = 500;
@@ -59,7 +61,6 @@ export default function TinyMCEEditor<KeyType extends string>({
const [ready, setReady] = React.useState(false); const [ready, setReady] = React.useState(false);
const [darkMode, setDarkMode] = React.useState(false); const [darkMode, setDarkMode] = React.useState(false);
const [refresh, setRefresh] = React.useState(0); const [refresh, setRefresh] = React.useState(0);
const [editor, setEditor] = React.useState<Editor>();
const title = name ? twuiSlugToNormalText(name) : "Rich Text"; const title = name ? twuiSlugToNormalText(name) : "Rich Text";
@@ -81,13 +82,28 @@ export default function TinyMCEEditor<KeyType extends string>({
const id = crypto.randomUUID(); 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(() => { React.useEffect(() => {
if (!editorComponentRef.current || !themeReady || !tinyMCE) { if (!editorComponentRef.current || !themeReady || !tinyMCE) {
return; return;
} }
const baseUrl = const baseUrl =
base_url ||
process.env.NEXT_PUBLIC_TINYMCE_BASE_URL || process.env.NEXT_PUBLIC_TINYMCE_BASE_URL ||
process.env.BUNEXT_PUBLIC_TINYMCE_BASE_URL ||
"https://www.datasquirel.com/tinymce-public"; "https://www.datasquirel.com/tinymce-public";
tinyMCE.init({ tinyMCE.init({
@@ -96,11 +112,10 @@ export default function TinyMCEEditor<KeyType extends string>({
plugins: plugins:
"advlist lists link image charmap preview anchor searchreplace visualblocks code fullscreen insertdatetime media table code help wordcount", "advlist lists link image charmap preview anchor searchreplace visualblocks code fullscreen insertdatetime media table code help wordcount",
toolbar: 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: content_style:
"body { font-family:Helvetica,Arial,sans-serif; font-size:14px; background-color: transparent }", "body { font-family:Helvetica,Arial,sans-serif; font-size:14px; background-color: transparent }",
init_instance_callback: (editor) => { init_instance_callback: (editor) => {
setEditor(editor as any);
if (editorRef) { if (editorRef) {
editorRef.current = editor; editorRef.current = editor;
passedSetEditor?.(editor); passedSetEditor?.(editor);
@@ -108,18 +123,12 @@ export default function TinyMCEEditor<KeyType extends string>({
if (defaultValue) editor.setContent(defaultValue); if (defaultValue) editor.setContent(defaultValue);
setReady(true); setReady(true);
// editor.on("change", (e) => { editor.on("change", (e) => {
// changeHandler?.(editor.getContent()); updateContent(editor);
// }); });
editor.on("input", (e) => { editor.on("input", (e) => {
if (changeHandler) { updateContent(editor);
window.clearTimeout(valueTimeout);
valueTimeout = setTimeout(() => {
changeHandler(editor.getContent());
}, EDITOR_VALUE_CHANGE_TIMEOUT);
}
}); });
if (useParentCSS) { if (useParentCSS) {
@@ -149,14 +158,6 @@ export default function TinyMCEEditor<KeyType extends string>({
}; };
}, [tinyMCE, themeReady, refresh, ...(refreshDependencyArray || [])]); }, [tinyMCE, themeReady, refresh, ...(refreshDependencyArray || [])]);
React.useEffect(() => {
const instance = editorRef.current;
if (instance) {
instance.setContent(defaultValue || "");
}
}, [defaultValue]);
return ( return (
<div <div
{...wrapperWrapperProps} {...wrapperWrapperProps}
@@ -165,9 +166,6 @@ export default function TinyMCEEditor<KeyType extends string>({
"bg-background-light dark:bg-background-dark", "bg-background-light dark:bg-background-dark",
wrapperWrapperProps?.className, wrapperWrapperProps?.className,
)} )}
onInput={(e) => {
console.log(`Input Detected`);
}}
> >
{showLabel && ( {showLabel && (
<label <label
File diff suppressed because it is too large Load Diff
+8 -2
View File
@@ -1,9 +1,13 @@
import React from "react"; import React from "react";
import { TinyMCE } from "./tinymce"; import type { TinyMCE } from "./tinymce";
let interval: any; 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 [tinyMCE, setTinyMCE] = React.useState<TinyMCE>();
const [refresh, setRefresh] = React.useState(0); const [refresh, setRefresh] = React.useState(0);
const [scriptLoaded, setScriptLoaded] = React.useState(false); const [scriptLoaded, setScriptLoaded] = React.useState(false);
@@ -21,7 +25,9 @@ export default function useTinyMCE() {
const script = document.createElement("script"); const script = document.createElement("script");
const baseUrl = const baseUrl =
params?.base_url ||
process.env.NEXT_PUBLIC_TINYMCE_BASE_URL || process.env.NEXT_PUBLIC_TINYMCE_BASE_URL ||
process.env.BUNEXT_PUBLIC_TINYMCE_BASE_URL ||
"https://www.datasquirel.com/tinymce-public"; "https://www.datasquirel.com/tinymce-public";
script.src = `${baseUrl}/tinymce.min.js`; 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"; import { twMerge } from "tailwind-merge";
export type TWUI_BORDER_PROPS = DetailedHTMLProps< export type TWUI_BORDER_PROPS = DetailedHTMLProps<
@@ -28,11 +28,11 @@ export default function Border({
? spacing == "normal" ? spacing == "normal"
? "px-3 py-2" ? "px-3 py-2"
: spacing == "tight" : spacing == "tight"
? "px-2 py-1" ? "px-2 py-1"
: "" : ""
: "px-3 py-2", : "px-3 py-2",
"twui-border", "twui-border",
props.className props.className,
)} )}
ref={componentRef} 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 Link from "../layout/Link";
import Divider from "../layout/Divider"; import Divider from "../layout/Divider";
import Row from "../layout/Row"; import Row from "../layout/Row";
@@ -47,7 +47,7 @@ export default function Breadcrumbs({
const [links, setLinks] = React.useState<LinkObject[] | null>( const [links, setLinks] = React.useState<LinkObject[] | null>(
pageUrl pageUrl
? twuiBreadcrumbsGenerateLinksFromUrl({ url: pageUrl, skipHome }) ? twuiBreadcrumbsGenerateLinksFromUrl({ url: pageUrl, skipHome })
: null : null,
); );
React.useEffect(() => { React.useEffect(() => {
@@ -76,13 +76,13 @@ export default function Breadcrumbs({
<nav <nav
className={twMerge( className={twMerge(
"overflow-x-auto", "overflow-x-auto",
"twui-current-breadcrumb-wrapper" "twui-current-breadcrumb-wrapper",
)} )}
aria-label="Breadcrumb" aria-label="Breadcrumb"
> >
<Row <Row
className={twMerge( 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 && ( {backButton && (
@@ -94,7 +94,7 @@ export default function Breadcrumbs({
className={twMerge( className={twMerge(
"p-1 -my-2 -mx-2", "p-1 -my-2 -mx-2",
"twui-breadcrumbs-back-button", "twui-breadcrumbs-back-button",
backButtonProps?.className backButtonProps?.className,
)} )}
onClick={(e) => { onClick={(e) => {
window.history.back(); window.history.back();
@@ -108,7 +108,7 @@ export default function Breadcrumbs({
vertical vertical
className={twMerge( className={twMerge(
"twui-breadcrumbs-divider", "twui-breadcrumbs-divider",
dividerProps?.className dividerProps?.className,
)} )}
/> />
)} )}
@@ -131,7 +131,7 @@ export default function Breadcrumbs({
isTarget ? "current" : "", isTarget ? "current" : "",
"twui-breadcrumb-link", "twui-breadcrumb-link",
linkProps?.className, linkProps?.className,
isTarget && currentLinkProps?.className isTarget && currentLinkProps?.className,
)} )}
title={ title={
currentLinkProps?.title || linkObject.title currentLinkProps?.title || linkObject.title
@@ -152,7 +152,7 @@ export default function Breadcrumbs({
isTarget ? "current" : "", isTarget ? "current" : "",
"twui-breadcrumb-link", "twui-breadcrumb-link",
linkProps?.className, linkProps?.className,
isTarget && currentLinkProps?.className isTarget && currentLinkProps?.className,
)} )}
> >
{currentLinkProps?.title || {currentLinkProps?.title ||
@@ -164,7 +164,7 @@ export default function Breadcrumbs({
{...dividerProps} {...dividerProps}
className={twMerge( className={twMerge(
"twui-breadcrumbs-divider", "twui-breadcrumbs-divider",
dividerProps?.className dividerProps?.className,
)} )}
/> />
)} )}
@@ -214,7 +214,7 @@ export function twuiBreadcrumbsGenerateLinksFromUrl({
for (let i = 0; i < array.length; i++) { for (let i = 0; i < array.length; i++) {
const lnText = array[i]; const lnText = array[i];
if (i > index || !lnText.match(/./)) continue; if (i > index || !lnText?.match(/./)) continue;
path += `/${lnText}`; path += `/${lnText}`;
} }
+5 -5
View File
@@ -1,7 +1,7 @@
import React, { import React, {
ComponentProps, type ComponentProps,
DetailedHTMLProps, type DetailedHTMLProps,
HTMLAttributes, type HTMLAttributes,
} from "react"; } from "react";
import { twMerge } from "tailwind-merge"; import { twMerge } from "tailwind-merge";
import Link from "../layout/Link"; 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", "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", "border border-slate-200 dark:border-white/10 border-solid",
noHover ? "" : "twui-card", noHover ? "" : "twui-card",
props.className props.className,
)} )}
> >
{props.children} {props.children}
@@ -60,7 +60,7 @@ export default function Card({
"cursor-pointer", "cursor-pointer",
"twui-card", "twui-card",
"twui-card-link", "twui-card-link",
linkProps?.className linkProps?.className,
)} )}
> >
{component} {component}
+3 -3
View File
@@ -1,5 +1,5 @@
import { ComponentProps, ReactNode } from "react"; import type { ComponentProps, ReactNode } from "react";
import Stack from "../layout/Stack copy"; import Stack from "../layout/Stack";
import Row from "../layout/Row"; import Row from "../layout/Row";
import { Check, CheckCircle, CheckCircle2 } from "lucide-react"; import { Check, CheckCircle, CheckCircle2 } from "lucide-react";
import Span from "../layout/Span"; import Span from "../layout/Span";
@@ -37,7 +37,7 @@ export default function CheckBulletPoints({
{...bulletWrapperProps} {...bulletWrapperProps}
className={twMerge( className={twMerge(
"gap-2 xl:flex-nowrap", "gap-2 xl:flex-nowrap",
bulletWrapperProps?.className bulletWrapperProps?.className,
)} )}
> >
{bulletPoint.icon || ( {bulletPoint.icon || (
+8 -8
View File
@@ -1,8 +1,8 @@
import { Check, Copy } from "lucide-react"; import { Check, Copy } from "lucide-react";
import React, { import React, {
DetailedHTMLProps, type DetailedHTMLProps,
HTMLAttributes, type HTMLAttributes,
PropsWithChildren, type PropsWithChildren,
} from "react"; } from "react";
import { twMerge } from "tailwind-merge"; import { twMerge } from "tailwind-merge";
import Stack from "../layout/Stack"; import Stack from "../layout/Stack";
@@ -56,7 +56,7 @@ export default function CodeBlock({
`rounded w-full transition-all items-start`, `rounded w-full transition-all items-start`,
"relative max-w-[80vw] sm:max-w-[85vw] xl:max-w-[880px]", "relative max-w-[80vw] sm:max-w-[85vw] xl:max-w-[880px]",
"twui-code-block-wrapper", "twui-code-block-wrapper",
wrapperProps?.className wrapperProps?.className,
)} )}
style={{ style={{
boxShadow: copied boxShadow: copied
@@ -69,14 +69,14 @@ export default function CodeBlock({
<Stack <Stack
className={twMerge( className={twMerge(
"gap-0 w-full overflow-x-auto relative", "gap-0 w-full overflow-x-auto relative",
"max-h-[600px] overflow-y-auto" "max-h-[600px] overflow-y-auto",
)} )}
> >
<Row <Row
className={twMerge( className={twMerge(
"w-full px-1 h-10 sticky top-0 py-2", "w-full px-1 h-10 sticky top-0 py-2",
singleBlock ? "absolute !bg-transparent" : "", singleBlock ? "absolute !bg-transparent" : "",
"twui-code-block-header" "twui-code-block-header",
)} )}
style={{ style={{
backgroundColor: finalBackgroundColor, backgroundColor: finalBackgroundColor,
@@ -128,7 +128,7 @@ export default function CodeBlock({
className={twMerge( className={twMerge(
`p-1 w-full [&_pre]:!bg-transparent`, `p-1 w-full [&_pre]:!bg-transparent`,
singleBlock ? "" : "-mt-1", singleBlock ? "" : "-mt-1",
"twui-code-pre-wrapper" "twui-code-pre-wrapper",
)} )}
ref={codeRef as any} ref={codeRef as any}
> >
@@ -138,7 +138,7 @@ export default function CodeBlock({
"!my-0 whitespace-pre-wrap", "!my-0 whitespace-pre-wrap",
language ? `language-${language}` : "", language ? `language-${language}` : "",
"twui-code-block-pre", "twui-code-block-pre",
props.className props.className,
)} )}
> >
{children} {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 { twMerge } from "tailwind-merge";
import { Moon, Sun } from "lucide-react"; import { Moon, Sun } from "lucide-react";
@@ -75,21 +75,21 @@ export default function ColorSchemeSelector({
className={twMerge( className={twMerge(
"flex flex-row items-center", "flex flex-row items-center",
"twui-color-scheme-selector", "twui-color-scheme-selector",
props.className props.className,
)} )}
> >
<button <button
title="Color Scheme Selector Button" title="Color Scheme Selector Button"
onClick={() => setActive(!active)} onClick={() => setActive(!active)}
className={twMerge( className={twMerge(
"cursor-pointer hover:opacity-70 flex items-center justify-center" "cursor-pointer hover:opacity-70 flex items-center justify-center",
)} )}
> >
<div <div
{...iconWrapperProps} {...iconWrapperProps}
className={twMerge( className={twMerge(
"w-6 h-6 flex items-center justify-center", "w-6 h-6 flex items-center justify-center",
iconWrapperProps?.className iconWrapperProps?.className,
)} )}
> >
{active == false && <Sun />} {active == false && <Sun />}
+2 -2
View File
@@ -1,10 +1,10 @@
import React, { import type {
ComponentProps, ComponentProps,
Dispatch, Dispatch,
ReactNode, ReactNode,
SetStateAction, SetStateAction,
} from "react"; } from "react";
import { Copy, LucideProps } from "lucide-react"; import { Copy, type LucideProps } from "lucide-react";
import Button from "../layout/Button"; import Button from "../layout/Button";
type Props = Omit<ComponentProps<typeof Button>, "title"> & { type Props = Omit<ComponentProps<typeof Button>, "title"> & {
+16 -16
View File
@@ -1,7 +1,7 @@
import React, { import React, {
DetailedHTMLProps, type DetailedHTMLProps,
HTMLAttributes, type HTMLAttributes,
PropsWithChildren, type PropsWithChildren,
} from "react"; } from "react";
import { twMerge } from "tailwind-merge"; import { twMerge } from "tailwind-merge";
@@ -104,7 +104,7 @@ export default function Dropdown({
className={twMerge( className={twMerge(
"flex flex-col items-center relative", "flex flex-col items-center relative",
"twui-dropdown-wrapper", "twui-dropdown-wrapper",
props.className props.className,
)} )}
onMouseEnter={() => { onMouseEnter={() => {
if (!hoverOpen) return; if (!hoverOpen) return;
@@ -142,7 +142,7 @@ export default function Dropdown({
className={twMerge( className={twMerge(
"cursor-pointer", "cursor-pointer",
"twui-dropdown-target", "twui-dropdown-target",
targetWrapperProps?.className targetWrapperProps?.className,
)} )}
> >
{target} {target}
@@ -155,20 +155,20 @@ export default function Dropdown({
position == "left" position == "left"
? "left-[100%] top-[50%] -translate-y-[50%]" ? "left-[100%] top-[50%] -translate-y-[50%]"
: position == "right" : position == "right"
? "right-[100%] top-[50%] -translate-y-[50%]" ? "right-[100%] top-[50%] -translate-y-[50%]"
: position == "bottom-left" : position == "bottom-left"
? "left-0 top-[100%]" ? "left-0 top-[100%]"
: position == "bottom-right" : position == "bottom-right"
? "right-0 top-[100%]" ? "right-0 top-[100%]"
: position == "center" : position == "center"
? "left-[50%] -translate-x-[50%] top-[100%]" ? "left-[50%] -translate-x-[50%] top-[100%]"
: position == "top" : position == "top"
? "left-[50%] -translate-x-[50%] bottom-[100%]" ? "left-[50%] -translate-x-[50%] bottom-[100%]"
: "top-[100%]", : "top-[100%]",
above ? "-translate-y-[120%]" : "", above ? "-translate-y-[120%]" : "",
open ? "flex" : "hidden", open ? "flex" : "hidden",
"twui-dropdown-content", "twui-dropdown-content",
contentWrapperProps?.className contentWrapperProps?.className,
)} )}
onMouseEnter={() => { onMouseEnter={() => {
if (!hoverOpen) return; 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 { twMerge } from "tailwind-merge";
import Stack from "../layout/Stack"; import Stack from "../layout/Stack";
import Border from "./Border"; import Border from "./Border";
@@ -46,7 +46,7 @@ export default function EmptyContent({
className={twMerge( className={twMerge(
"w-full", "w-full",
borderProps?.className, borderProps?.className,
"twui-empty-content-border" "twui-empty-content-border",
)} )}
> >
<Center> <Center>
@@ -57,7 +57,7 @@ export default function EmptyContent({
{...textProps} {...textProps}
className={twMerge( className={twMerge(
"opacity-70 text-foreground-light dark:text-foreground-dark", "opacity-70 text-foreground-light dark:text-foreground-dark",
textProps?.className textProps?.className,
)} )}
> >
{title} {title}
@@ -75,7 +75,7 @@ export default function EmptyContent({
className={twMerge( className={twMerge(
"w-full", "w-full",
"twui-empty-content-link", "twui-empty-content-link",
linkProps?.className linkProps?.className,
)} )}
href={url} 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 Link from "../layout/Link";
import { TwuiHeaderLink } from "./HeaderNav"; import type { TwuiHeaderLink } from "./HeaderNav";
import { twMerge } from "tailwind-merge"; import { twMerge } from "tailwind-merge";
import Row from "../layout/Row"; import Row from "../layout/Row";
@@ -21,7 +21,7 @@ export default function HeaderLink({ link, ...props }: TWUI_HEADER_LINK_PROPS) {
className={twMerge( className={twMerge(
"grow p-2 hover:opacity-50", "grow p-2 hover:opacity-50",
"twui-header-link", "twui-header-link",
props.className props.className,
)} )}
> >
<Row> <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 { twMerge } from "tailwind-merge";
import Row from "../layout/Row"; import Row from "../layout/Row";
import HeaderNavLinkComponent from "./HeaderNavLinkComponent"; import HeaderNavLinkComponent from "./HeaderNavLinkComponent";
@@ -44,13 +48,13 @@ export default function HeaderNav({
{...props} {...props}
className={twMerge( className={twMerge(
"twui-header-nav w-full xl:w-auto", "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"> <Row className="gap-x-2 gap-y-2 flex-col xl:flex-row items-start xl:items-stretch">
{headerLinks.map((link, index) => { {headerLinks.map((link, index) => {
const targetCustomDropdown = customDropdowns?.find( const targetCustomDropdown = customDropdowns?.find(
(d) => d.url == link.url (d) => d.url == link.url,
); );
return ( return (
<HeaderNavLinkComponent <HeaderNavLinkComponent
@@ -1,10 +1,10 @@
import React, { DetailedHTMLProps, HTMLAttributes, ReactNode } from "react"; import React, { type ReactNode } from "react";
import { twMerge } from "tailwind-merge"; import { twMerge } from "tailwind-merge";
import Row from "../layout/Row"; import Row from "../layout/Row";
import HeaderLink from "./HeaderLink"; import HeaderLink from "./HeaderLink";
import { ChevronDown } from "lucide-react"; import { ChevronDown } from "lucide-react";
import Dropdown from "./Dropdown"; import Dropdown from "./Dropdown";
import { TwuiHeaderLink } from "./HeaderNav"; import type { TwuiHeaderLink } from "./HeaderNav";
import Card from "./Card"; import Card from "./Card";
import Stack from "../layout/Stack"; import Stack from "../layout/Stack";
import Button from "../layout/Button"; import Button from "../layout/Button";
@@ -31,7 +31,7 @@ export default function HeaderNavLinkComponent({
<ChevronDown <ChevronDown
className={twMerge( className={twMerge(
"hidden xl:flex xl:-ml-1", "hidden xl:flex xl:-ml-1",
"twui-header-nav-link-icon" "twui-header-nav-link-icon",
)} )}
size={20} size={20}
/> />
@@ -45,7 +45,7 @@ export default function HeaderNavLinkComponent({
<div <div
className={twMerge( className={twMerge(
"relative w-full xl:w-auto [&_a.active]:font-bold", "relative w-full xl:w-auto [&_a.active]:font-bold",
"twui-header-nav-link-component" "twui-header-nav-link-component",
)} )}
> >
{isDropdown ? ( {isDropdown ? (
@@ -62,7 +62,7 @@ export default function HeaderNavLinkComponent({
> >
<ChevronDown <ChevronDown
className={twMerge( className={twMerge(
"twui-header-nav-link-icon !text-link dark:!text-white" "twui-header-nav-link-icon !text-link dark:!text-white",
)} )}
size={20} size={20}
/> />
@@ -77,7 +77,7 @@ export default function HeaderNavLinkComponent({
<Card <Card
className={twMerge( className={twMerge(
"w-full p-0", "w-full p-0",
"twui-header-nav-link-dropdown" "twui-header-nav-link-dropdown",
)} )}
> >
<Stack className="w-full items-stretch gap-0 py-2"> <Stack className="w-full items-stretch gap-0 py-2">
@@ -90,7 +90,7 @@ export default function HeaderNavLinkComponent({
className="px-6 py-4" className="px-6 py-4"
/> />
); );
} },
)} )}
</Stack> </Stack>
</Card> </Card>
@@ -113,7 +113,7 @@ export default function HeaderNavLinkComponent({
<Card <Card
className={twMerge( className={twMerge(
"min-w-[200px] mt-2 p-0", "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"> <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 HtmlToReact from "html-to-react";
import { twMerge } from "tailwind-merge"; import { twMerge } from "tailwind-merge";
+10 -9
View File
@@ -1,8 +1,8 @@
import React, { import React, {
ComponentProps, type ComponentProps,
DetailedHTMLProps, type DetailedHTMLProps,
HTMLAttributes, type HTMLAttributes,
ReactNode, type ReactNode,
} from "react"; } from "react";
import { twMerge } from "tailwind-merge"; import { twMerge } from "tailwind-merge";
import Link from "../layout/Link"; import Link from "../layout/Link";
@@ -25,15 +25,16 @@ export type TWUI_LINK_LIST_LINK_OBJECT = {
onClick?: React.MouseEventHandler<HTMLElement>; 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< export type TWUI_LINK_LIST_PROPS = DetailedHTMLProps<
HTMLAttributes<HTMLDivElement>, HTMLAttributes<HTMLDivElement>,
HTMLDivElement HTMLDivElement
> & { > & {
links: ( links: TWUILink[];
| TWUI_LINK_LIST_LINK_OBJECT
| TWUI_LINK_LIST_LINK_OBJECT[]
| undefined
)[];
linkProps?: ComponentProps<typeof Link>; linkProps?: ComponentProps<typeof Link>;
buttonProps?: Omit<ComponentProps<typeof Button>, "title">; buttonProps?: Omit<ComponentProps<typeof Button>, "title">;
divider?: boolean; 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"; import { twMerge } from "tailwind-merge";
type Props = DetailedHTMLProps< 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"; import { twMerge } from "tailwind-merge";
/** /**
@@ -15,7 +15,7 @@ export default function LoadingBlock({
"bg-slate-200 dark:bg-white/10", "bg-slate-200 dark:bg-white/10",
"rounded animate-pulse w-full h-[60px]", "rounded animate-pulse w-full h-[60px]",
"twui-loading-block", "twui-loading-block",
props.className props.className,
)} )}
> >
{props.children} {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 { twMerge } from "tailwind-merge";
import Center from "../layout/Center"; import Center from "../layout/Center";
import Loading from "./Loading"; 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 ModalComponent from "../(partials)/ModalComponent";
import PopoverComponent from "../(partials)/PopoverComponent"; import PopoverComponent from "../(partials)/PopoverComponent";
import { twMerge } from "tailwind-merge"; import { twMerge } from "tailwind-merge";
@@ -33,6 +33,7 @@ export type TWUI_MODAL_PROPS = DetailedHTMLProps<
debounce?: number; debounce?: number;
onClose?: () => any; onClose?: () => any;
hoverOpen?: boolean; hoverOpen?: boolean;
no_cancel_button?: boolean;
}; };
/** /**
@@ -43,6 +44,8 @@ export type TWUI_MODAL_PROPS = DetailedHTMLProps<
* @ID twui-popover-root * @ID twui-popover-root
* @className twui-popover-content * @className twui-popover-content
* @className twui-popover-target * @className twui-popover-target
* @className twui-modal-content
* @className twui-modal-target
*/ */
export default function Modal(props: TWUI_MODAL_PROPS) { export default function Modal(props: TWUI_MODAL_PROPS) {
const { const {
@@ -57,6 +60,7 @@ export default function Modal(props: TWUI_MODAL_PROPS) {
debounce = 500, debounce = 500,
onClose, onClose,
hoverOpen, hoverOpen,
no_cancel_button,
} = props; } = props;
const [ready, setReady] = React.useState(false); const [ready, setReady] = React.useState(false);
@@ -94,23 +98,19 @@ export default function Modal(props: TWUI_MODAL_PROPS) {
const popoverTargetActiveRef = React.useRef(false); const popoverTargetActiveRef = React.useRef(false);
const popoverContentActiveRef = React.useRef(false); const popoverContentActiveRef = React.useRef(false);
let closeTimeout: any; const closeTimeoutRef = React.useRef<any>(null);
const popoverEnterFn = React.useCallback((e: any) => { const popoverEnterFn = React.useCallback((e: any) => {
popoverTargetActiveRef.current = true; popoverTargetActiveRef.current = true;
popoverContentActiveRef.current = false; popoverContentActiveRef.current = false;
window.clearTimeout(closeTimeoutRef.current);
setOpen(true); setOpen(true);
props.onMouseEnter?.(e); props.onMouseEnter?.(e);
}, []); }, []);
const popoverLeaveFn = React.useCallback((e: any) => { const popoverLeaveFn = React.useCallback((e: any) => {
window.clearTimeout(closeTimeout); window.clearTimeout(closeTimeoutRef.current);
closeTimeout = setTimeout(() => { closeTimeoutRef.current = setTimeout(() => {
// if (popoverTargetActiveRef.current) {
// popoverTargetActiveRef.current = false;
// return;
// }
if (popoverContentActiveRef.current) { if (popoverContentActiveRef.current) {
popoverContentActiveRef.current = false; popoverContentActiveRef.current = false;
return; return;
@@ -120,11 +120,22 @@ export default function Modal(props: TWUI_MODAL_PROPS) {
props.onMouseLeave?.(e); 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 handleClickOutside = React.useCallback((e: MouseEvent) => {
const targetEl = e.target as HTMLElement; const targetEl = e.target as HTMLElement;
const closestWrapper = targetEl.closest(".twui-popover-content"); if (!isPopover) {
const closestTarget = targetEl.closest(".twui-popover-target"); return;
}
const closestWrapper = targetEl.closest(`.${content_class_name}`);
const closestTarget = targetEl.closest(`.${target_class_name}`);
if (closestTarget) return; if (closestTarget) return;
@@ -163,8 +174,8 @@ export default function Modal(props: TWUI_MODAL_PROPS) {
: targetWrapperProps?.onMouseLeave : targetWrapperProps?.onMouseLeave
} }
className={twMerge( className={twMerge(
"twui-popover-target", target_class_name,
targetWrapperProps?.className targetWrapperProps?.className,
)} )}
> >
{target} {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 _ from "lodash";
import { ChevronLeft, ChevronRight } from "lucide-react"; import { ChevronLeft, ChevronRight } from "lucide-react";
import { twMerge } from "tailwind-merge"; import { twMerge } from "tailwind-merge";
@@ -35,12 +35,30 @@ export default function Pagination({
const pages = Math.ceil(count / limit); 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 ( return (
<Row <Row
{...props} {...props}
className={twMerge( className={twMerge(
"w-full justify-between flex-nowrap", "w-full justify-between md:flex-nowrap",
props.className props.className,
)} )}
> >
{pages > 1 && ( {pages > 1 && (
@@ -54,21 +72,31 @@ export default function Pagination({
size="small" size="small"
className={twMerge( className={twMerge(
"p-1", "p-1",
page == 1 ? "opacity-40 pointer-events-none" : "" page == 1 ? "opacity-40 pointer-events-none" : "",
)} )}
> >
<ChevronLeft size={20} /> <ChevronLeft size={20} />
</Button> </Button>
)} )}
<Row className={twMerge("gap-6 w-full flex-nowrap justify-center")}> {pages > 1 && <div className="flex md:hidden">{next_button}</div>}
<Span size="small" variant="faded">
<Row
className={twMerge(
"gap-6 w-full md:flex-nowrap justify-center",
)}
>
<Span
size="small"
variant="faded"
className="whitespace-nowrap"
>
Page {page} / {pages} Page {page} / {pages}
</Span> </Span>
{pages > 1 && ( {pages > 1 && (
<Row <Row
className={twMerge( className={twMerge(
"flex-nowrap overflow-x-auto p-1 max-w-[90%]" "flex-nowrap overflow-x-auto p-1 max-w-[90%]",
)} )}
> >
{Array(pages) {Array(pages)
@@ -92,7 +120,7 @@ export default function Pagination({
size="small" size="small"
color={isCurrent ? "primary" : "gray"} color={isCurrent ? "primary" : "gray"}
className={twMerge( className={twMerge(
"p-1 w-6 h-6 min-w-6" "p-1 w-6 h-6 min-w-6",
)} )}
key={index} key={index}
> >
@@ -104,23 +132,7 @@ export default function Pagination({
)} )}
</Row> </Row>
{pages > 1 && ( {pages > 1 && <div className="hidden md:flex">{next_button}</div>}
<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>
)}
</Row> </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"; 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", "border border-slate-200 dark:border-white/10 border-solid w-full",
"relative", "relative",
"twui-paper", "twui-paper",
props.className props.className,
)} )}
> >
{props.children} {props.children}
+1 -2
View File
@@ -1,5 +1,4 @@
import React, { DetailedHTMLProps, HTMLAttributes } from "react"; import Modal, { type TWUI_MODAL_PROPS } from "./Modal";
import Modal, { TWUI_MODAL_PROPS } from "./Modal";
/** /**
* # Popover Component * # Popover Component
+1 -1
View File
@@ -2,7 +2,7 @@ import React from "react";
import { serialize } from "next-mdx-remote/serialize"; import { serialize } from "next-mdx-remote/serialize";
import remarkGfm from "remark-gfm"; import remarkGfm from "remark-gfm";
import rehypePrismPlus from "rehype-prism-plus"; 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"; import { useMDXComponents } from "../mdx/mdx-components";
export const TWUIPrismLanguages = ["shell", "javascript"] as const; export const TWUIPrismLanguages = ["shell", "javascript"] as const;
+44 -33
View File
@@ -1,9 +1,13 @@
import { twMerge } from "tailwind-merge"; import { twMerge } from "tailwind-merge";
import Input, { InputProps } from "../form/Input"; import Input from "../form/Input";
import Button from "../layout/Button"; import Button from "../layout/Button";
import Row from "../layout/Row"; import Row from "../layout/Row";
import { Search as SearchIcon } from "lucide-react"; 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; let timeout: any;
@@ -14,14 +18,13 @@ export type SearchProps<KeyType extends string> = DetailedHTMLProps<
dispatch?: (value?: string) => void; dispatch?: (value?: string) => void;
changeHandler?: (value?: string) => void; changeHandler?: (value?: string) => void;
delay?: number; delay?: number;
inputProps?: InputProps<KeyType>; inputProps?: ComponentProps<typeof Input>;
buttonProps?: DetailedHTMLProps< buttonProps?: Omit<ComponentProps<typeof Button>, "title">;
React.ButtonHTMLAttributes<HTMLButtonElement>,
HTMLButtonElement
>;
loading?: boolean; loading?: boolean;
placeholder?: string; placeholder?: string;
componentRef?: React.RefObject<HTMLInputElement | null>; componentRef?: React.RefObject<HTMLInputElement | null>;
beforeSearchIconComponent?: ReactNode;
no_search_button?: boolean;
}; };
/** /**
@@ -39,10 +42,12 @@ export default function Search<KeyType extends string>({
loading, loading,
placeholder, placeholder,
componentRef, componentRef,
beforeSearchIconComponent,
no_search_button,
...props ...props
}: SearchProps<KeyType>) { }: SearchProps<KeyType>) {
const [input, setInput] = React.useState( const [input, setInput] = React.useState(
props.defaultValue?.toString() || "" props.defaultValue?.toString() || "",
); );
React.useEffect(() => { React.useEffect(() => {
@@ -68,7 +73,7 @@ export default function Search<KeyType extends string>({
className={twMerge( className={twMerge(
"relative xl:flex-nowrap items-stretch gap-0 flex-nowrap", "relative xl:flex-nowrap items-stretch gap-0 flex-nowrap",
"twui-search-wrapper", "twui-search-wrapper",
props?.className props?.className,
)} )}
> >
<Input <Input
@@ -76,39 +81,45 @@ export default function Search<KeyType extends string>({
placeholder={placeholder || "Search"} placeholder={placeholder || "Search"}
{...inputProps} {...inputProps}
value={input} value={input}
onChange={(e) => setInput(e.target.value)} onChange={(e) => {
setInput(e.target.value);
inputProps?.onChange?.(e as any);
}}
className={twMerge( className={twMerge(
"rounded-r-none!", "rounded-r-none!",
"twui-search-input", "twui-search-input",
inputProps?.className inputProps?.className,
)} )}
wrapperProps={{ wrapperProps={{
className: "rounded-r-none!", className: "rounded-r-none!",
}} }}
componentRef={inputRef} componentRef={inputRef}
/> />
<Button {beforeSearchIconComponent}
loadingProps={{ size: "small" }} {no_search_button ? null : (
{...buttonProps} <Button
variant="outlined" loadingProps={{ size: "small" }}
color="gray" {...buttonProps}
className={twMerge( variant="outlined"
"rounded-l-none! ml-[1px]", color="gray"
"twui-search-button", className={twMerge(
buttonProps?.className "rounded-l-none! ml-[1px]",
)} "twui-search-button",
onClick={() => { buttonProps?.className,
dispatch?.(input); )}
changeHandler?.(input); onClick={() => {
}} dispatch?.(input);
title="Search Button" changeHandler?.(input);
loading={loading} }}
> title="Search Button"
<SearchIcon loading={loading}
className="text-slate-800 dark:text-white" >
size={20} <SearchIcon
/> className="text-slate-800 dark:text-white"
</Button> size={15}
/>
</Button>
)}
</Row> </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"; import { twMerge } from "tailwind-merge";
type Props = PropsWithChildren & 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]:absolute [&_.twui-code-block-header]:!bg-transparent",
"[&_.twui-code-block-header]:mt-2 [&_.twui-code-block-header]:pr-3 [&_.twui-divider]:hidden", "[&_.twui-code-block-header]:mt-2 [&_.twui-code-block-header]:pr-3 [&_.twui-divider]:hidden",
"[&_pre]:!pr-14 [&_.twui-code-block-copied-text]:!hidden", "[&_pre]:!pr-14 [&_.twui-code-block-copied-text]:!hidden",
props.className props.className,
)} )}
> >
{children} {children}
+2 -7
View File
@@ -1,10 +1,5 @@
import { LucideProps, Star } from "lucide-react"; import { type LucideProps, Star } from "lucide-react";
import React, { import React, { type DetailedHTMLProps, type HTMLAttributes } from "react";
DetailedHTMLProps,
ForwardRefExoticComponent,
HTMLAttributes,
RefAttributes,
} from "react";
import { twMerge } from "tailwind-merge"; import { twMerge } from "tailwind-merge";
type StarProps = { 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 ( return (
<div className={twMerge("overflow-x-auto w-full")}> <div className={twMerge("overflow-x-auto w-full")}>
<table <table
className={twMerge( 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"> <thead className="bg-gray dark:bg-gray-dark">
@@ -32,7 +32,7 @@ export default function Table({ data }: Props) {
key={header} key={header}
className={twMerge( className={twMerge(
"px-3 py-2 text-left opacity-50", "px-3 py-2 text-left opacity-50",
"font-semibold" "font-semibold",
)} )}
title={header} title={header}
> >
@@ -44,14 +44,14 @@ export default function Table({ data }: Props) {
<tbody <tbody
className={twMerge( className={twMerge(
"bg-background-light dark:bg-background-dark", "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) => ( {data.map((row, index) => (
<tr <tr
key={index} key={index}
className={twMerge( 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) => ( {headers.map((header) => (
@@ -60,7 +60,7 @@ export default function Table({ data }: Props) {
className={twMerge( className={twMerge(
"px-3 py-2 whitespace-nowrap text-foreground-light", "px-3 py-2 whitespace-nowrap text-foreground-light",
"dark:text-foreground-dark max-w-[200px] overflow-hidden", "dark:text-foreground-dark max-w-[200px] overflow-hidden",
"overflow-ellipsis" "overflow-ellipsis",
)} )}
title={row[header]} 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 { twMerge } from "tailwind-merge";
import Border from "./Border"; import Border from "./Border";
import Stack from "../layout/Stack"; 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"; import { twMerge } from "tailwind-merge";
import {
export type TWUITabsObject = { ButtonClasses,
title: string; type TWUIButtonColorType,
value: string; type TWUIButtonVariantType,
content: React.ReactNode; } from "../layout/Button";
defaultActive?: boolean;
};
export type TWUI_TOGGLE_PROPS = PropsWithChildren & export type TWUI_TOGGLE_PROPS = PropsWithChildren &
React.DetailedHTMLProps< React.DetailedHTMLProps<
React.HTMLAttributes<HTMLDivElement>, React.HTMLAttributes<HTMLDivElement>,
HTMLDivElement HTMLDivElement
> & { > & {
color?: "normal" | "secondary" | "error" | "success" | "gray"; color?: TWUIButtonColorType;
variant?: "normal" | "outlined" | "ghost"; variant?: TWUIButtonVariantType;
href?: string; href?: string;
newTab?: boolean; newTab?: boolean;
linkProps?: React.DetailedHTMLProps< linkProps?: React.DetailedHTMLProps<
@@ -37,50 +35,18 @@ export default function Tag({
linkProps, linkProps,
...props ...props
}: TWUI_TOGGLE_PROPS) { }: TWUI_TOGGLE_PROPS) {
const finalClassName =
ButtonClasses[variant || "normal"]?.[color || "primary"] ?? "";
const mainComponent = ( const mainComponent = (
<div <div
{...props} {...props}
className={twMerge( className={twMerge(
"text-xs px-2 py-0.5 rounded-full outline-0", "text-xs px-2 py-0.5 rounded-full outline-0",
"text-center flex items-center justify-center", "text-center flex items-center justify-center",
color == "secondary" finalClassName,
? "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")
: "",
"twui-tag", "twui-tag",
props.className props.className,
)} )}
> >
{children} {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 { twMerge } from "tailwind-merge";
import Card from "./Card"; import Card from "./Card";
import { X } from "lucide-react"; 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"; import { twMerge } from "tailwind-merge";
export type TWUI_TOGGLE_PROPS = DetailedHTMLProps< export type TWUI_TOGGLE_PROPS = DetailedHTMLProps<
HTMLAttributes<HTMLDivElement>, HTMLAttributes<HTMLDivElement>,
HTMLDivElement HTMLDivElement
> & { > & {
active?: boolean;
setActive?: React.Dispatch<React.SetStateAction<boolean | undefined>>;
circleProps?: DetailedHTMLProps< circleProps?: DetailedHTMLProps<
HTMLAttributes<HTMLDivElement>, HTMLAttributes<HTMLDivElement>,
HTMLDivElement HTMLDivElement
>; >;
default_active?: boolean;
changeHandler?: (checked: boolean) => void;
}; };
/** /**
@@ -20,10 +25,24 @@ export type TWUI_TOGGLE_PROPS = DetailedHTMLProps<
*/ */
export default function Toggle({ export default function Toggle({
circleProps, circleProps,
active, default_active,
setActive, changeHandler,
...props ...props
}: TWUI_TOGGLE_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 ( return (
<div <div
{...props} {...props}
@@ -32,9 +51,11 @@ export default function Toggle({
"border border-slate-300 dark:border-white/30 border-solid rounded-full", "border border-slate-300 dark:border-white/30 border-solid rounded-full",
active ? "justify-end" : "justify-start", active ? "justify-end" : "justify-start",
"twui-toggle-wrapper", "twui-toggle-wrapper",
props.className props.className,
)} )}
onClick={() => setActive?.(!active)} onClick={() => {
setActive(!active);
}}
> >
{typeof active == "undefined" ? ( {typeof active == "undefined" ? (
<div className="w-3.5 h-3.5 twui-toggle-circle"></div> <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-blue-600 dark:bg-blue-500"
: "bg-slate-300 dark:bg-white/40", : "bg-slate-300 dark:bg-white/40",
"twui-toggle-circle", "twui-toggle-circle",
circleProps?.className circleProps?.className,
)} )}
></div> ></div>
)} )}
@@ -1,10 +1,11 @@
import { Send, X } from "lucide-react"; import { Send, X } from "lucide-react";
import React, { Dispatch, SetStateAction } from "react"; import React, { type Dispatch, type SetStateAction } from "react";
import { ChatCompletionMessageParam } from "openai/resources/index"; import type { ChatCompletionMessageParam } from "openai/resources/index";
import Row from "../../layout/Row"; import Row from "../../layout/Row";
import Button from "../../layout/Button"; import Button from "../../layout/Button";
import CopySlug from "../CopySlug"; import CopySlug from "../CopySlug";
import AIPromptHistoryModal from "./AIPromptHistoryModal"; import AIPromptHistoryModal from "./AIPromptHistoryModal";
import AIVoiceTranscription from "./AIVoiceTranscription";
type Props = { type Props = {
streamRes: string; streamRes: string;
@@ -12,10 +13,15 @@ type Props = {
setPrompt: Dispatch<SetStateAction<string>>; setPrompt: Dispatch<SetStateAction<string>>;
loading: boolean; loading: boolean;
promptFn: (prompt: string) => void; promptFn: (prompt: string) => void;
voice_transcription_fn?: (params: { last_message?: string }) => void;
transcribing?: boolean;
history: ChatCompletionMessageParam[]; history: ChatCompletionMessageParam[];
prompt: string; prompt: string;
currentPromptRef: React.MutableRefObject<string>; currentPromptRef: React.MutableRefObject<string>;
promptInputRef: React.RefObject<HTMLTextAreaElement>; promptInputRef: React.RefObject<HTMLTextAreaElement>;
user_image_url?: string;
last_response?: string;
cancel_voice_transcription_fn?: () => void;
}; };
export default function AIPromptActionSection({ export default function AIPromptActionSection({
@@ -28,10 +34,24 @@ export default function AIPromptActionSection({
setPrompt, setPrompt,
currentPromptRef, currentPromptRef,
promptInputRef, promptInputRef,
user_image_url,
transcribing,
voice_transcription_fn,
last_response,
cancel_voice_transcription_fn,
}: Props) { }: Props) {
return ( return (
<Row className="w-full justify-between"> <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(/./) && ( {streamRes.match(/./) && (
<React.Fragment> <React.Fragment>
<Button <Button
@@ -53,10 +73,13 @@ export default function AIPromptActionSection({
/> />
</React.Fragment> </React.Fragment>
)} )}
<AIPromptHistoryModal
history={history}
user_image_url={user_image_url}
/>
</Row> </Row>
<Row>
<AIPromptHistoryModal history={history} />
</Row>
<Row> <Row>
<Button <Button
title="Send Prompt" title="Send Prompt"
@@ -74,6 +97,7 @@ export default function AIPromptActionSection({
promptFn(prompt); promptFn(prompt);
}} }}
loadingProps={{ size: "smaller" }} loadingProps={{ size: "smaller" }}
disabled={transcribing}
/> />
</Row> </Row>
</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 React from "react";
import Paper from "../Paper"; import Paper from "../Paper";
import Stack from "../../layout/Stack"; import Stack from "../../layout/Stack";
import AIPromptPreview from "./AIPromptPreview"; import AIPromptPreview from "./AIPromptPreview";
import LoadingOverlay from "../LoadingOverlay";
import Textarea from "../../form/Textarea";
import AIPromptActionSection from "./AIPromptActionSection";
import Card from "../Card"; import Card from "../Card";
import Row from "../../layout/Row"; import Row from "../../layout/Row";
import Span from "../../layout/Span"; import Span from "../../layout/Span";
import { MessageCircleMore } from "lucide-react"; import { MessageCircleMore } from "lucide-react";
import AIPromptBlockInputSection from "./AIPromptBlockInputSection";
type Props = { type Props = {
model?: string; model?: string;
@@ -57,47 +55,17 @@ export default function AIPromptBlock({
history={history} history={history}
loading={loading} loading={loading}
/> />
<Stack className="w-full relative"> <AIPromptBlockInputSection
{loading && <LoadingOverlay />} {...{
<Textarea model,
placeholder={ promptFn,
placeholder || history,
(model ? `Prompt ${model}` : "Prompt AI") loading,
} mdRes,
wrapperProps={{ className: "outline-none" }} setMdRes,
wrapperWrapperProps={{ className: "w-full" }} placeholder,
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>
</Stack> </Stack>
</Paper> </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 type { ChatCompletionMessageParam } from "openai/resources/index";
import { ChatCompletionMessageParam } from "openai/resources/index";
import { twMerge } from "tailwind-merge"; import { twMerge } from "tailwind-merge";
import { Bot, User } from "lucide-react"; import { Bot, History, User } from "lucide-react";
import Modal from "../Modal"; import Modal from "../Modal";
import Button from "../../layout/Button"; import Button from "../../layout/Button";
import Stack from "../../layout/Stack"; import Stack from "../../layout/Stack";
@@ -11,13 +10,20 @@ import Divider from "../../layout/Divider";
import Row from "../../layout/Row"; import Row from "../../layout/Row";
import Card from "../Card"; import Card from "../Card";
import Border from "../Border"; 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 = { type Props = {
history: ChatCompletionMessageParam[]; 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; if (!history[0]) return null;
return ( return (
@@ -27,9 +33,10 @@ export default function AIPromptHistoryModal({ history }: Props) {
title="View Chat History" title="View Chat History"
size="smaller" size="smaller"
color="gray" color="gray"
variant="outlined" variant="ghost"
className="p-1!"
> >
View History <History size={20} opacity={0.7} />
</Button> </Button>
} }
className="max-w-[900px] bg-slate-100 dark:bg-white/5 xl:p-8" 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 ( return (
<Row <Row
key={index} key={index}
className="w-full items-start justify-end" className="w-full items-start justify-end flex-nowrap"
> >
<Card <Card
className={twMerge( 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> </Card>
<Border className="w-10 h-10 rounded-full p-2 items-center justify-center"> {user_image_url ? (
<User /> <Img
</Border> 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> </Row>
); );
} }
return ( return (
<Row <Row
key={index} key={index}
@@ -74,21 +94,18 @@ export default function AIPromptHistoryModal({ history }: Props) {
<Border <Border
className={twMerge( className={twMerge(
"w-10 h-10 rounded-full items-center justify-center bg-white p-2", "w-10 h-10 rounded-full items-center justify-center bg-white p-2",
"dark:bg-background-dark" "dark:bg-background-dark",
)} )}
> >
<Bot /> <Bot />
</Border> </Border>
</Stack> </Stack>
<Card className="grow overflow-x-auto xl:p-8"> <Card className="grow overflow-x-auto xl:p-8">
<MarkdownEditorPreviewComponent <Stack className="twui-llm-chat-block">
value={hst.content?.toString() || ""} <ReactMarkdown remarkPlugins={[remarkGfm]}>
maxHeight="none" {hst.content?.toString()}
wrapperProps={{ </ReactMarkdown>
className: </Stack>
"border-none p-0 ai-response-content w-full",
}}
/>
</Card> </Card>
</Row> </Row>
); );
+1 -1
View File
@@ -2,7 +2,7 @@ import Divider from "../../layout/Divider";
import Stack from "../../layout/Stack"; import Stack from "../../layout/Stack";
import React from "react"; import React from "react";
import MarkdownEditorPreviewComponent from "../../mdx/markdown/MarkdownEditorPreviewComponent"; import MarkdownEditorPreviewComponent from "../../mdx/markdown/MarkdownEditorPreviewComponent";
import { ChatCompletionMessageParam } from "openai/resources/index"; import type { ChatCompletionMessageParam } from "openai/resources/index";
type Props = { type Props = {
streamRes: string; 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, { import React, {
ComponentProps, type ComponentProps,
DetailedHTMLProps, type DetailedHTMLProps,
HTMLAttributes, type HTMLAttributes,
ReactNode, type ReactNode,
} from "react"; } from "react";
import { twMerge } from "tailwind-merge"; import { twMerge } from "tailwind-merge";
import CheckMarkSVG from "../svgs/CheckMarkSVG"; import CheckMarkSVG from "../svgs/CheckMarkSVG";
@@ -36,6 +36,7 @@ export type CheckboxProps = Omit<
noLabel?: boolean; noLabel?: boolean;
size?: number; size?: number;
changeHandler?: (value: boolean) => void; changeHandler?: (value: boolean) => void;
clickDispatchHandler?: (value: boolean) => void;
info?: string | ReactNode; info?: string | ReactNode;
wrapperWrapperProps?: ComponentProps<typeof Stack>; wrapperWrapperProps?: ComponentProps<typeof Stack>;
}; };
@@ -61,6 +62,7 @@ export default function Checkbox({
wrapperWrapperProps, wrapperWrapperProps,
noLabel, noLabel,
title, title,
clickDispatchHandler,
...props ...props
}: CheckboxProps) { }: CheckboxProps) {
const finalSize = size || 20; const finalSize = size || 20;
@@ -95,9 +97,10 @@ export default function Checkbox({
wrapperClassName, wrapperClassName,
wrapperProps?.className, wrapperProps?.className,
)} )}
onClick={() => { onClick={(e) => {
setChecked(!checked); setChecked(!checked);
externalSetChecked?.(!checked); externalSetChecked?.(!checked);
clickDispatchHandler?.(!checked);
}} }}
> >
<div <div
+8 -4
View File
@@ -1,11 +1,15 @@
import Button from "../layout/Button"; import Button from "../layout/Button";
import Stack from "../layout/Stack"; import Stack from "../layout/Stack";
import { FileArchive, FilePlus2, X } from "lucide-react"; 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 Card from "../elements/Card";
import Span from "../layout/Span"; import Span from "../layout/Span";
import Center from "../layout/Center"; import Center from "../layout/Center";
import { FileInputToBase64FunctionReturn } from "../utils/form/fileInputToBase64"; import type { FileInputToBase64FunctionReturn } from "../utils/form/fileInputToBase64";
import { twMerge } from "tailwind-merge"; import { twMerge } from "tailwind-merge";
import fileInputToBase64 from "../utils/form/fileInputToBase64"; import fileInputToBase64 from "../utils/form/fileInputToBase64";
import Row from "../layout/Row"; import Row from "../layout/Row";
@@ -149,7 +153,7 @@ export default function FileUpload({
for (let i = 0; i < files.length; i++) { for (let i = 0; i < files.length; i++) {
const file = files[i]; const file = files[i];
const fileObj = await fileInputToBase64({ const fileObj = await fileInputToBase64({
inputFile: file, inputFile: file!,
}); });
filesArr.push(fileObj); filesArr.push(fileObj);
} }
@@ -330,7 +334,7 @@ export default function FileUpload({
} }
}); });
} else { } else {
inputFile = e.dataTransfer.files?.[0]; inputFile = e.dataTransfer.files?.[0]!;
} }
if (!inputFile) return; if (!inputFile) return;
+1 -1
View File
@@ -1,5 +1,5 @@
import _ from "lodash"; import _ from "lodash";
import { DetailedHTMLProps, FormHTMLAttributes, RefObject } from "react"; import type { DetailedHTMLProps, FormHTMLAttributes, RefObject } from "react";
import { twMerge } from "tailwind-merge"; import { twMerge } from "tailwind-merge";
type Props<T extends { [key: string]: any } = { [key: string]: any }> = type Props<T extends { [key: string]: any } = { [key: string]: any }> =
+53 -40
View File
@@ -1,12 +1,12 @@
import Button from "../layout/Button"; import Button from "../layout/Button";
import Stack from "../layout/Stack"; import Stack from "../layout/Stack";
import { ImagePlus, X } from "lucide-react"; import { ImagePlus, X } from "lucide-react";
import React, { DetailedHTMLProps } from "react"; import React, { type DetailedHTMLProps } from "react";
import Card from "../elements/Card"; import Card from "../elements/Card";
import Span from "../layout/Span"; import Span from "../layout/Span";
import Center from "../layout/Center"; import Center from "../layout/Center";
import imageInputToBase64, { import imageInputToBase64, {
ImageInputToBase64FunctionReturn, type ImageInputToBase64FunctionReturn,
} from "../utils/form/imageInputToBase64"; } from "../utils/form/imageInputToBase64";
import { twMerge } from "tailwind-merge"; import { twMerge } from "tailwind-merge";
import Tag from "../elements/Tag"; import Tag from "../elements/Tag";
@@ -20,6 +20,9 @@ type ImageUploadProps = DetailedHTMLProps<
onChangeHandler?: ( onChangeHandler?: (
imgData: ImageInputToBase64FunctionReturn | undefined, imgData: ImageInputToBase64FunctionReturn | undefined,
) => any; ) => any;
multipleOnChangeHandler?: (
imgData: ImageInputToBase64FunctionReturn[] | undefined,
) => any;
fileInputProps?: DetailedHTMLProps< fileInputProps?: DetailedHTMLProps<
React.InputHTMLAttributes<HTMLInputElement>, React.InputHTMLAttributes<HTMLInputElement>,
HTMLInputElement HTMLInputElement
@@ -50,6 +53,7 @@ type ImageUploadProps = DetailedHTMLProps<
setImgURL?: React.Dispatch<React.SetStateAction<string | undefined>>; setImgURL?: React.Dispatch<React.SetStateAction<string | undefined>>;
externalImage?: ImageInputToBase64FunctionReturn; externalImage?: ImageInputToBase64FunctionReturn;
restoreImageFn?: () => void; restoreImageFn?: () => void;
upload_only?: boolean;
}; };
/** /**
@@ -71,6 +75,8 @@ export default function ImageUpload({
restoreImageFn, restoreImageFn,
setLoading, setLoading,
setImgURL, setImgURL,
multipleOnChangeHandler,
upload_only,
...props ...props
}: ImageUploadProps) { }: ImageUploadProps) {
const [imageObject, setImageObject] = React.useState< const [imageObject, setImageObject] = React.useState<
@@ -119,7 +125,7 @@ export default function ImageUpload({
}); });
imgArr.push(fileObj); imgArr.push(fileObj);
} }
multipleOnChangeHandler?.(imgArr);
externalSetImages?.(imgArr); externalSetImages?.(imgArr);
setLoading?.(false); setLoading?.(false);
})(); })();
@@ -208,43 +214,50 @@ export default function ImageUpload({
<Span size="smaller" variant="faded"> <Span size="smaller" variant="faded">
{label || "Click to Upload Image"} {label || "Click to Upload Image"}
</Span> </Span>
<Stack className="cancel-upload w-full items-stretch gap-1"> {upload_only ? null : (
<Input <>
placeholder="Eg. https://example.com/img.png" <Stack className="cancel-upload w-full items-stretch gap-1">
className="text-sm twui-image-url-input" <Input
title="Enter Image URL" placeholder="URL Eg. https://example.com/img.png"
wrapperWrapperProps={{ className: "mt-2" }} className="text-sm twui-image-url-input"
changeHandler={(value) => { title="Enter Image URL"
imageUrlRef.current = value; wrapperWrapperProps={{
}} className: "mt-2",
showLabel }}
/> changeHandler={(value) => {
<Button imageUrlRef.current = value;
title="Restore Image Button" }}
size="smaller" />
variant="outlined" <Button
color="gray" title="Restore Image Button"
onClick={() => { size="smaller"
if (!imageUrlRef.current) return; variant="outlined"
setSrc(imageUrlRef.current); color="gray"
}} onClick={() => {
> if (!imageUrlRef.current)
Set Image URL return;
</Button> setSrc(imageUrlRef.current);
</Stack> }}
{existingImageUrl && ( className="opacity-70"
<Button >
title="Restore Image Button" Set Image URL
size="smaller" </Button>
variant="ghost" </Stack>
onClick={() => { {existingImageUrl && (
restoreImageFn?.() || <Button
setSrc(existingImageUrl); title="Restore Image Button"
}} size="smaller"
className="cancel-upload" variant="ghost"
> onClick={() => {
Restore Original Image restoreImageFn?.() ||
</Button> setSrc(existingImageUrl);
}}
className="cancel-upload"
>
Restore Original Image
</Button>
)}
</>
)} )}
</Stack> </Stack>
</Center> </Center>
+1 -1
View File
@@ -2,7 +2,7 @@ import React, { useEffect, useRef, useState } from "react";
import Row from "../../layout/Row"; import Row from "../../layout/Row";
import { Info, Minus, Plus } from "lucide-react"; import { Info, Minus, Plus } from "lucide-react";
import twuiNumberfy from "../../utils/numberfy"; import twuiNumberfy from "../../utils/numberfy";
import { InputProps } from "."; import type { InputProps } from ".";
let pressInterval: any; let pressInterval: any;
let pressTimeout: any; let pressTimeout: any;
+67 -19
View File
@@ -1,16 +1,16 @@
import React, { import React, {
ComponentProps, type ComponentProps,
DetailedHTMLProps, type DetailedHTMLProps,
InputHTMLAttributes, type InputHTMLAttributes,
LabelHTMLAttributes, type LabelHTMLAttributes,
ReactNode, type ReactNode,
RefObject, type RefObject,
TextareaHTMLAttributes, type TextareaHTMLAttributes,
useRef, useRef,
} from "react"; } from "react";
import { twMerge } from "tailwind-merge"; import { twMerge } from "tailwind-merge";
import Span from "../../layout/Span"; 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 { AutocompleteOptions } from "../../types";
import twuiNumberfy from "../../utils/numberfy"; import twuiNumberfy from "../../utils/numberfy";
import Dropdown from "../../elements/Dropdown"; import Dropdown from "../../elements/Dropdown";
@@ -19,7 +19,7 @@ import NumberInputButtons from "./NumberInputButtons";
import twuiSlugToNormalText from "../../utils/slug-to-normal-text"; import twuiSlugToNormalText from "../../utils/slug-to-normal-text";
import Row from "../../layout/Row"; import Row from "../../layout/Row";
import Paper from "../../elements/Paper"; import Paper from "../../elements/Paper";
import { TWUISelectValidityObject } from "../Select"; import type { TWUISelectValidityObject } from "../Select";
export type InputProps<KeyType extends string> = Omit< export type InputProps<KeyType extends string> = Omit<
DetailedHTMLProps<InputHTMLAttributes<HTMLInputElement>, HTMLInputElement>, DetailedHTMLProps<InputHTMLAttributes<HTMLInputElement>, HTMLInputElement>,
@@ -75,6 +75,7 @@ export type InputProps<KeyType extends string> = Omit<
HTMLDivElement HTMLDivElement
>; >;
// refreshDefaultValue?: number; // refreshDefaultValue?: number;
copy?: boolean;
}; };
let refreshes = 0; let refreshes = 0;
@@ -117,6 +118,7 @@ export default function Input<KeyType extends string>(
clearInputProps, clearInputProps,
rawNumber, rawNumber,
// refreshDefaultValue, // refreshDefaultValue,
copy,
...props ...props
} = inputProps; } = inputProps;
@@ -163,7 +165,7 @@ export default function Input<KeyType extends string>(
numberText ? "text" : props.type, numberText ? "text" : props.type,
); );
const DEFAULT_DEBOUNCE = 500; const DEFAULT_DEBOUNCE = 200;
const finalDebounce = debounce || DEFAULT_DEBOUNCE; const finalDebounce = debounce || DEFAULT_DEBOUNCE;
const finalLabel = const finalLabel =
label || label ||
@@ -225,11 +227,12 @@ export default function Input<KeyType extends string>(
} }
}; };
const isControlled = props.value !== undefined && props.value !== null;
React.useEffect(() => { React.useEffect(() => {
// if (!existingReady) return; if (!isControlled) return;
if (typeof props.value !== "string" || !props.value.match(/./)) return; setValue(String(props.value ?? ""));
setValue(String(props.value)); }, [props.value, isControlled]);
}, [props.value]);
// React.useEffect(() => { // React.useEffect(() => {
// if (!refreshDefaultValue) return; // if (!refreshDefaultValue) return;
@@ -290,8 +293,8 @@ export default function Input<KeyType extends string>(
onChange={handleValueChange} onChange={handleValueChange}
autoComplete={autoComplete} autoComplete={autoComplete}
rows={props.height ? Number(props.height) : props.rows || 2} rows={props.height ? Number(props.height) : props.rows || 2}
defaultValue={defaultInitialValue} defaultValue={isControlled ? undefined : defaultInitialValue}
value={props.value ? getFinalValue(props.value) : undefined} value={isControlled ? getFinalValue(props.value) : undefined}
/> />
) : ( ) : (
<input <input
@@ -318,9 +321,9 @@ export default function Input<KeyType extends string>(
}} }}
onChange={handleValueChange} onChange={handleValueChange}
type={inputType} type={inputType}
defaultValue={defaultInitialValue} defaultValue={isControlled ? undefined : defaultInitialValue}
autoComplete={autoComplete} 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} {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 <div
title="Clear Input Field" title="Clear Input Field"
{...clearInputProps} {...clearInputProps}
@@ -502,3 +527,26 @@ export default function Input<KeyType extends string>(
</Stack> </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, ComponentProps,
DetailedHTMLProps, DetailedHTMLProps,
InputHTMLAttributes, 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, { import React, {
ComponentProps, type ComponentProps,
DetailedHTMLProps, type DetailedHTMLProps,
Dispatch, type Dispatch,
InputHTMLAttributes, type InputHTMLAttributes,
LabelHTMLAttributes, type LabelHTMLAttributes,
ReactNode, type ReactNode,
RefObject, type RefObject,
SelectHTMLAttributes, type SelectHTMLAttributes,
SetStateAction, type SetStateAction,
} from "react"; } from "react";
import { twMerge } from "tailwind-merge"; import { twMerge } from "tailwind-merge";
import Row from "../layout/Row"; import Row from "../layout/Row";
@@ -18,6 +18,8 @@ import Span from "../layout/Span";
import Stack from "../layout/Stack"; import Stack from "../layout/Stack";
import twuiSlugify from "../utils/slugify"; import twuiSlugify from "../utils/slugify";
import twuiSlugToNormalText from "../utils/slug-to-normal-text"; import twuiSlugToNormalText from "../utils/slug-to-normal-text";
import Input from "./Input";
import Button from "../layout/Button";
export type TWUISelectValidityObject = { export type TWUISelectValidityObject = {
isValid?: boolean; isValid?: boolean;
@@ -26,7 +28,7 @@ export type TWUISelectValidityObject = {
export type TWUISelectOptionObject< export type TWUISelectOptionObject<
KeyType extends string = string, KeyType extends string = string,
T extends { [k: string]: any } = { [k: string]: any } T extends { [k: string]: any } = { [k: string]: any },
> = { > = {
title?: string; title?: string;
value: KeyType; value: KeyType;
@@ -36,7 +38,7 @@ export type TWUISelectOptionObject<
export type TWUISelectProps< export type TWUISelectProps<
KeyType extends string, KeyType extends string,
T extends { [k: string]: any } = { [k: string]: any } T extends { [k: string]: any } = { [k: string]: any },
> = DetailedHTMLProps< > = DetailedHTMLProps<
SelectHTMLAttributes<HTMLSelectElement>, SelectHTMLAttributes<HTMLSelectElement>,
HTMLSelectElement HTMLSelectElement
@@ -60,11 +62,13 @@ export type TWUISelectProps<
validateValueFn?: (value: string) => Promise<TWUISelectValidityObject>; validateValueFn?: (value: string) => Promise<TWUISelectValidityObject>;
dispatchState?: Dispatch<SetStateAction<T | undefined>>; dispatchState?: Dispatch<SetStateAction<T | undefined>>;
name?: KeyType; name?: KeyType;
input_props?: ComponentProps<typeof Input>;
option_button_props?: Omit<ComponentProps<typeof Button>, "title">;
}; };
export type TWUISelectValueObject< export type TWUISelectValueObject<
KeyType extends string, KeyType extends string,
T extends { [k: string]: any } = { [k: string]: any } T extends { [k: string]: any } = { [k: string]: any },
> = { > = {
value: KeyType; value: KeyType;
data?: T; data?: T;
@@ -78,7 +82,7 @@ export type TWUISelectValueObject<
*/ */
export default function Select< export default function Select<
KeyType extends string, KeyType extends string,
T extends { [k: string]: any } = { [k: string]: any } T extends { [k: string]: any } = { [k: string]: any },
>({ >({
label, label,
options, options,
@@ -102,9 +106,9 @@ export default function Select<
const [value, setValue] = React.useState<TWUISelectValueObject<KeyType, T>>( const [value, setValue] = React.useState<TWUISelectValueObject<KeyType, T>>(
{ {
value: options[0]?.value, value: options[0]?.value!,
data: options[0]?.data, data: options[0]?.data,
} },
); );
React.useEffect(() => { React.useEffect(() => {
@@ -128,10 +132,10 @@ export default function Select<
const selectID = label const selectID = label
? twuiSlugify(label) ? twuiSlugify(label)
: props.name : props.name
? twuiSlugify(props.name) ? twuiSlugify(props.name)
: props.title : props.title
? twuiSlugify(props.title) ? twuiSlugify(props.title)
: `select-${Math.round(Math.random() * 1000000)}`; : `select-${Math.round(Math.random() * 1000000)}`;
return ( return (
<Stack <Stack
@@ -148,7 +152,7 @@ export default function Select<
"focus:outline-slate-700 dark:focus:outline-white/50", "focus:outline-slate-700 dark:focus:outline-white/50",
"bg-white dark:bg-background-dark", "bg-white dark:bg-background-dark",
validity.isValid ? "" : "outline-warning border-warning", validity.isValid ? "" : "outline-warning border-warning",
wrapperProps?.className wrapperProps?.className,
)} )}
> >
{showLabel && ( {showLabel && (
@@ -159,7 +163,7 @@ export default function Select<
"text-xs absolute -top-2.5 left-2 text-foreground-light/80 bg-background-light", "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", "dark:text-foreground-dark/70 dark:bg-background-dark px-1.5 rounded-t",
"twui-input-label", "twui-input-label",
labelProps?.className labelProps?.className,
)} )}
> >
{label || props.title || props.name} {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", "w-full pl-3 py-2 rounded-default appearance-none pr-8",
"grow !border-none !outline-none", "grow !border-none !outline-none",
"twui-select", "twui-select",
props.className props.className,
)} )}
ref={selectRef} ref={selectRef}
value={ value={
@@ -183,7 +187,7 @@ export default function Select<
} }
onChange={(e) => { onChange={(e) => {
const targetValue = options.find( const targetValue = options.find(
(opt) => opt.value == e.target.value (opt) => opt.value == e.target.value,
); );
if (targetValue) { if (targetValue) {
@@ -192,7 +196,7 @@ export default function Select<
changeHandler?.( changeHandler?.(
e.target.value as (typeof options)[number]["value"], e.target.value as (typeof options)[number]["value"],
targetValue?.data targetValue?.data,
); );
props.onChange?.(e); props.onChange?.(e);
@@ -219,7 +223,7 @@ export default function Select<
{...iconProps} {...iconProps}
className={twMerge( className={twMerge(
"pointer-events-none -ml-6", "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 * # Textarea Component
+1 -1
View File
@@ -29,7 +29,7 @@ export default function useIntersectionObserver({
const entry = entries[0]; const entry = entries[0];
window.clearTimeout(timeoutRef.current); window.clearTimeout(timeoutRef.current);
if (entry.isIntersecting) { if (entry?.isIntersecting) {
timeoutRef.current = setTimeout(() => { timeoutRef.current = setTimeout(() => {
setIsIntersecting(true); setIsIntersecting(true);
+11 -2
View File
@@ -1,14 +1,20 @@
import React from "react"; import React from "react";
export type TWUIErrorType = {
msg?: string;
};
type Params = { type Params = {
initialLoading?: boolean; initialLoading?: boolean;
initialReady?: boolean; initialReady?: boolean;
initialOpen?: boolean; initialOpen?: boolean;
}; };
export type UseStatusStatusType = { export type TWUIStatusType = {
msg?: string; msg?: string;
error?: boolean; error?: boolean;
success?: boolean;
loading?: boolean;
}; };
export default function useStatus(params?: Params) { export default function useStatus(params?: Params) {
@@ -16,9 +22,10 @@ export default function useStatus(params?: Params) {
const [loading, setLoading] = React.useState( const [loading, setLoading] = React.useState(
params?.initialLoading || false, params?.initialLoading || false,
); );
const [status, setStatus] = React.useState<UseStatusStatusType>({}); const [status, setStatus] = React.useState<TWUIStatusType>();
const [ready, setReady] = React.useState(params?.initialReady || false); const [ready, setReady] = React.useState(params?.initialReady || false);
const [open, setOpen] = React.useState(params?.initialOpen || false); const [open, setOpen] = React.useState(params?.initialOpen || false);
const [error, setError] = React.useState<TWUIErrorType>();
return { return {
refresh, refresh,
@@ -31,5 +38,7 @@ export default function useStatus(params?: Params) {
setReady, setReady,
open, open,
setOpen, 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 { 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 Link from "./Link";
import Row from "./Row"; import Row from "./Row";
+65 -26
View File
@@ -1,32 +1,50 @@
import React, { import React, {
AnchorHTMLAttributes, type AnchorHTMLAttributes,
ButtonHTMLAttributes, type ButtonHTMLAttributes,
ComponentProps, type ComponentProps,
DetailedHTMLProps, type DetailedHTMLProps,
HTMLAttributeAnchorTarget, type HTMLAttributeAnchorTarget,
HTMLAttributes, type HTMLAttributes,
} from "react"; } from "react";
import { twMerge } from "tailwind-merge"; import { twMerge } from "tailwind-merge";
import Loading from "../elements/Loading"; 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< export type TWUIButtonProps = DetailedHTMLProps<
ButtonHTMLAttributes<HTMLButtonElement>, ButtonHTMLAttributes<HTMLButtonElement>,
HTMLButtonElement HTMLButtonElement
> & { > & {
title: string; title: string;
variant?: "normal" | "ghost" | "outlined"; variant?: TWUIButtonVariantType;
color?: color?: TWUIButtonColorType;
| "primary" size?: TWUIButtonSizeType;
| "secondary"
| "text"
| "white"
| "accent"
| "gray"
| "error"
| "warning"
| "success";
size?: "small" | "smaller" | "normal" | "large" | "larger";
loadingIconSize?: React.ComponentProps<typeof Loading>["size"]; loadingIconSize?: React.ComponentProps<typeof Loading>["size"];
href?: string; href?: string;
target?: HTMLAttributeAnchorTarget; target?: HTMLAttributeAnchorTarget;
@@ -37,6 +55,10 @@ export type TWUIButtonProps = DetailedHTMLProps<
>; >;
beforeIcon?: TWUILucideIconName | React.JSX.Element; beforeIcon?: TWUILucideIconName | React.JSX.Element;
afterIcon?: 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< buttonContentProps?: DetailedHTMLProps<
HTMLAttributes<HTMLDivElement>, HTMLAttributes<HTMLDivElement>,
HTMLDivElement HTMLDivElement
@@ -44,7 +66,7 @@ export type TWUIButtonProps = DetailedHTMLProps<
loadingProps?: ComponentProps<typeof Loading>; loadingProps?: ComponentProps<typeof Loading>;
}; };
const buttonClasses: Record<string, Record<string, string>> = { export const ButtonClasses: Record<string, Record<string, string>> = {
normal: { normal: {
primary: twMerge( primary: twMerge(
"bg-primary hover:bg-primary-hover text-white", "bg-primary hover:bg-primary-hover text-white",
@@ -96,8 +118,8 @@ const buttonClasses: Record<string, Record<string, string>> = {
"twui-button-accent-outlined", "twui-button-accent-outlined",
), ),
gray: twMerge( gray: twMerge(
"bg-transparent outline outline-1 outline-slate-300", "bg-transparent outline outline-1 outline-gray",
"text-slate-600 dark:text-white/60 dark:outline-white/30", "text-dark dark:text-white/60 dark:outline-white/30",
"twui-button-gray-outlined", "twui-button-gray-outlined",
), ),
white: twMerge( white: twMerge(
@@ -110,6 +132,11 @@ const buttonClasses: Record<string, Record<string, string>> = {
"dark:outline-error dark:text-error-dark", "dark:outline-error dark:text-error-dark",
"twui-button-error-outlined", "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: { ghost: {
primary: twMerge( primary: twMerge(
@@ -226,9 +253,14 @@ export default function Button({
loading, loading,
loadingIconSize, loadingIconSize,
loadingProps, loadingProps,
before_icon_name,
after_icon_name,
before_icon_props,
after_icon_props,
...props ...props
}: TWUIButtonProps) { }: TWUIButtonProps) {
const finalClassName = buttonClasses[variant || "normal"]?.[color || "primary"] ?? ""; const finalClassName =
ButtonClasses[variant || "normal"]?.[color || "primary"] ?? "";
const buttonComponent = ( const buttonComponent = (
<button <button
@@ -246,7 +278,7 @@ export default function Button({
? "text-lg twui-button-large" ? "text-lg twui-button-large"
: size == "larger" : size == "larger"
? "px-5 py-3 text-xl twui-button-larger" ? "px-5 py-3 text-xl twui-button-larger"
: "twui-button-base", : "text-sm twui-button-base",
finalClassName, finalClassName,
loading ? "pointer-events-none opacity-80" : "", loading ? "pointer-events-none opacity-80" : "",
props.className, props.className,
@@ -262,7 +294,12 @@ export default function Button({
buttonContentProps?.className, buttonContentProps?.className,
)} )}
> >
{beforeIcon ? ( {before_icon_name ? (
<LucideIcon
{...before_icon_props}
name={before_icon_name}
/>
) : beforeIcon ? (
typeof beforeIcon == "string" ? ( typeof beforeIcon == "string" ? (
<LucideIcon name={beforeIcon as TWUILucideIconName} /> <LucideIcon name={beforeIcon as TWUILucideIconName} />
) : ( ) : (
@@ -270,7 +307,9 @@ export default function Button({
) )
) : null} ) : null}
{props.children} {props.children}
{afterIcon ? ( {after_icon_name ? (
<LucideIcon {...after_icon_props} name={after_icon_name} />
) : afterIcon ? (
typeof afterIcon == "string" ? ( typeof afterIcon == "string" ? (
<LucideIcon name={afterIcon as TWUILucideIconName} /> <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"; import { twMerge } from "tailwind-merge";
/** /**
@@ -14,7 +14,7 @@ export default function Center({
className={twMerge( className={twMerge(
"flex flex-col items-center justify-center gap-4 p-2 w-full", "flex flex-col items-center justify-center gap-4 p-2 w-full",
"h-full twui-center", "h-full twui-center",
props.className props.className,
)} )}
> >
{props.children} {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"; import { twMerge } from "tailwind-merge";
/** /**
@@ -15,7 +15,7 @@ export default function Container({
"flex w-full max-w-container gap-4 justify-between", "flex w-full max-w-container gap-4 justify-between",
"flex-wrap flex-col xl:flex-row items-start xl:items-center", "flex-wrap flex-col xl:flex-row items-start xl:items-center",
"twui-container", "twui-container",
props.className props.className,
)} )}
> >
{props.children} {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"; import { twMerge } from "tailwind-merge";
type Props = DetailedHTMLProps< type Props = DetailedHTMLProps<
@@ -27,7 +27,7 @@ export default function Divider({ vertical, dashed, ...props }: Props) {
"twui-divider", "twui-divider",
vertical ? "twui-divider-vertical" : "twui-divider-horizontal", vertical ? "twui-divider-vertical" : "twui-divider-horizontal",
dashed ? "border-dashed" : "border-solid", dashed ? "border-dashed" : "border-solid",
props.className props.className,
)} )}
/> />
); );
+2 -2
View File
@@ -1,5 +1,5 @@
import { twMerge } from "tailwind-merge"; import { twMerge } from "tailwind-merge";
import Img, { TWUIImageProps } from "./Img"; import Img, { type TWUIImageProps } from "./Img";
import Border from "../elements/Border"; import Border from "../elements/Border";
export default function DocsImg({ ...props }: TWUIImageProps) { export default function DocsImg({ ...props }: TWUIImageProps) {
@@ -9,7 +9,7 @@ export default function DocsImg({ ...props }: TWUIImageProps) {
{...props} {...props}
className={twMerge( className={twMerge(
"w-full h-auto rounded-default overflow-hidden", "w-full h-auto rounded-default overflow-hidden",
props.className props.className,
)} )}
/> />
</Border> </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"; import { twMerge } from "tailwind-merge";
/** /**
@@ -16,7 +16,7 @@ export default function Footer({
"px-4 sm:px-10 py-2", "px-4 sm:px-10 py-2",
"border-0 border-b border-slate-200 border-solid", "border-0 border-b border-slate-200 border-solid",
"twui-footer", "twui-footer",
props.className props.className,
)} )}
> >
{props.children} {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"; import { twMerge } from "tailwind-merge";
/** /**
@@ -15,7 +15,7 @@ export default function H1({
"text-4xl md:text-5xl mb-4", "text-4xl md:text-5xl mb-4",
"twui-headings twui-heading", "twui-headings twui-heading",
"twui-h1", "twui-h1",
props.className props.className,
)} )}
> >
{props.children} {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"; import { twMerge } from "tailwind-merge";
/** /**
@@ -15,7 +15,7 @@ export default function H2({
"text-2xl md:text-3xl mb-4", "text-2xl md:text-3xl mb-4",
"twui-headings twui-heading", "twui-headings twui-heading",
"twui-h2", "twui-h2",
props.className props.className,
)} )}
> >
{props.children} {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"; import { twMerge } from "tailwind-merge";
/** /**
@@ -15,7 +15,7 @@ export default function H3({
"text-xl mb-4", "text-xl mb-4",
"twui-headings twui-heading", "twui-headings twui-heading",
"twui-h3", "twui-h3",
props.className props.className,
)} )}
> >
{props.children} {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"; import { twMerge } from "tailwind-merge";
/** /**
@@ -15,7 +15,7 @@ export default function H4({
"text-base mb-4", "text-base mb-4",
"twui-headings twui-heading", "twui-headings twui-heading",
"twui-h4", "twui-h4",
props.className props.className,
)} )}
> >
{props.children} {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"; import { twMerge } from "tailwind-merge";
/** /**
@@ -15,7 +15,7 @@ export default function H5({
"mb-4", "mb-4",
"twui-headings twui-heading", "twui-headings twui-heading",
"twui-h5", "twui-h5",
props.className props.className,
)} )}
> >
{props.children} {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"; import { twMerge } from "tailwind-merge";
/** /**
@@ -14,7 +14,7 @@ export default function HR({
className={twMerge( className={twMerge(
"border-slate-200 dark:border-white/10 w-full my-4", "border-slate-200 dark:border-white/10 w-full my-4",
"twui-hr", "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"; import { twMerge } from "tailwind-merge";
/** /**
@@ -16,7 +16,7 @@ export default function Header({
"px-4 sm:px-10 py-3 flex-wrap", "px-4 sm:px-10 py-3 flex-wrap",
"border-0 border-b border-slate-200 dark:border-white/10 border-solid", "border-0 border-b border-slate-200 dark:border-white/10 border-solid",
"twui-header", "twui-header",
props.className props.className,
)} )}
> >
{props.children} {props.children}
+2 -2
View File
@@ -1,5 +1,5 @@
import { ComponentProps, ReactNode } from "react"; import type { ComponentProps, ReactNode } from "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 Link from "./Link";
import Row from "./Row"; import Row from "./Row";
+22 -4
View File
@@ -1,5 +1,10 @@
import _ from "lodash"; 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"; import { twMerge } from "tailwind-merge";
export type TWUIImageProps = DetailedHTMLProps< export type TWUIImageProps = DetailedHTMLProps<
@@ -18,6 +23,7 @@ export type TWUIImageProps = DetailedHTMLProps<
imgErrComp?: ReactNode; imgErrComp?: ReactNode;
imgErrSrcLight?: string; imgErrSrcLight?: string;
imgErrSrcDark?: string; imgErrSrcDark?: string;
checkImageFn?: () => Promise<boolean> | boolean;
}; };
/** /**
@@ -29,6 +35,7 @@ export default function Img({
imgErrComp, imgErrComp,
imgErrSrcDark, imgErrSrcDark,
imgErrSrcLight, imgErrSrcLight,
checkImageFn,
...props ...props
}: TWUIImageProps) { }: TWUIImageProps) {
const width = props.size || props.width; 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" ? "absolute top-0 left-0 w-full h-full object-cover z-0"
: "", : "",
"twui-img", "twui-img",
props.className props.className,
), ),
onError: (e) => { onError: (e) => {
if (props.fallbackImageSrc) { 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) { if (imageError) {
return ( return (
imgErrComp || ( imgErrComp || (
@@ -101,7 +119,7 @@ export default function Img({
{...interpolatedProps} {...interpolatedProps}
className={twMerge( className={twMerge(
"hidden dark:block", "hidden dark:block",
interpolatedProps.className interpolatedProps.className,
)} )}
src={props.srcDark} src={props.srcDark}
onError={(e) => { onError={(e) => {
@@ -114,7 +132,7 @@ export default function Img({
{...interpolatedProps} {...interpolatedProps}
className={twMerge( className={twMerge(
"block dark:hidden", "block dark:hidden",
interpolatedProps.className interpolatedProps.className,
)} )}
src={props.srcLight} src={props.srcLight}
onError={(e) => { 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 { twMerge } from "tailwind-merge";
import { ArrowUpRight, LucideProps } from "lucide-react"; import { ArrowUpRight, type LucideProps } from "lucide-react";
type Props = DetailedHTMLProps< type Props = DetailedHTMLProps<
AnchorHTMLAttributes<HTMLAnchorElement>, AnchorHTMLAttributes<HTMLAnchorElement>,
@@ -33,7 +37,7 @@ export default function Link({
"border-0 border-b border-link dark:border-link-dark border-solid leading-4", "border-0 border-b border-link dark:border-link-dark border-solid leading-4",
"twui-anchor", "twui-anchor",
"twui-a", "twui-a",
props.className props.className,
)} )}
onClick={(e) => { onClick={(e) => {
const target = e.target as HTMLElement; const target = e.target as HTMLElement;
@@ -52,7 +56,7 @@ export default function Link({
{...arrowProps} {...arrowProps}
className={twMerge( className={twMerge(
"inline-block ml-1 -mt-[1px]", "inline-block ml-1 -mt-[1px]",
arrowProps?.className arrowProps?.className,
)} )}
/> />
)} )}
+1 -1
View File
@@ -1,4 +1,4 @@
import { import type {
AnchorHTMLAttributes, AnchorHTMLAttributes,
DetailedHTMLProps, DetailedHTMLProps,
HTMLAttributes, 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"; import { twMerge } from "tailwind-merge";
export type LoadingRectangleBlockProps = DetailedHTMLProps< 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"; import { twMerge } from "tailwind-merge";
/** /**
@@ -14,7 +14,7 @@ export default function Main({
className={twMerge( className={twMerge(
"flex flex-col items-center w-full", "flex flex-col items-center w-full",
"twui-main", "twui-main",
props.className props.className,
)} )}
> >
{props.children} {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"; import { twMerge } from "tailwind-merge";
type Props = DetailedHTMLProps< type Props = DetailedHTMLProps<
@@ -21,7 +21,7 @@ export default function P({ noMargin, ...props }: Props) {
noMargin ? "!m-0 p-0" : "", noMargin ? "!m-0 p-0" : "",
"twui-p", "twui-p",
"twui-paragraph", "twui-paragraph",
props.className props.className,
)} )}
> >
{props.children} {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"; import { twMerge } from "tailwind-merge";
type Props = DetailedHTMLProps< type Props = DetailedHTMLProps<
@@ -22,7 +22,7 @@ export default function Row({ noWrap, itemsStart, ...props }: Props) {
noWrap ? "xl:flex-nowrap" : "flex-wrap", noWrap ? "xl:flex-nowrap" : "flex-wrap",
itemsStart ? "items-start" : "items-center", itemsStart ? "items-start" : "items-center",
"twui-row", "twui-row",
props.className props.className,
)} )}
> >
{props.children} {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"; import { twMerge } from "tailwind-merge";
/** /**
@@ -15,7 +15,7 @@ export default function Section({
"flex flex-col items-center w-full", "flex flex-col items-center w-full",
"px-4 sm:px-10 py-10", "px-4 sm:px-10 py-10",
"twui-section", "twui-section",
props.className props.className,
)} )}
> >
{props.children} {props.children}
+2 -2
View File
@@ -1,5 +1,5 @@
import _ from "lodash"; import _ from "lodash";
import { DetailedHTMLProps, HTMLAttributes } from "react"; import type { DetailedHTMLProps, HTMLAttributes } from "react";
import { twMerge } from "tailwind-merge"; import { twMerge } from "tailwind-merge";
type Props = DetailedHTMLProps< type Props = DetailedHTMLProps<
@@ -21,7 +21,7 @@ export default function Spacer({ horizontal, ...props }: Props) {
"", "",
horizontal ? "w-10" : "w-full h-10", horizontal ? "w-10" : "w-full h-10",
"twui-spacer", "twui-spacer",
props.className props.className,
)} )}
> >
{props.children} {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"; 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 _ from "lodash";
import { DetailedHTMLProps, HTMLAttributes } from "react"; import type { DetailedHTMLProps, HTMLAttributes } from "react";
import { twMerge } from "tailwind-merge"; import { twMerge } from "tailwind-merge";
type Props = DetailedHTMLProps< type Props = DetailedHTMLProps<
@@ -29,7 +29,7 @@ export default function Stack({ gap, componentRef, ...props }: Props) {
: `gap-${gap}` : `gap-${gap}`
: "", : "",
"twui-stack", "twui-stack",
props.className props.className,
)} )}
ref={componentRef} 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 { twMerge } from "tailwind-merge";
import MarkdownEditorPreviewComponent from "./MarkdownEditorPreviewComponent"; import MarkdownEditorPreviewComponent from "./MarkdownEditorPreviewComponent";
import MarkdownEditorComponent from "./MarkdownEditorComponent"; import MarkdownEditorComponent from "./MarkdownEditorComponent";
@@ -73,7 +73,6 @@ export default function MarkdownEditor({
setValue={setValue} setValue={setValue}
value={value} value={value}
maxHeight={maxHeight} maxHeight={maxHeight}
refreshDepArr={[refresh]}
{...editorProps} {...editorProps}
/> />
<MarkdownEditorPreviewComponent <MarkdownEditorPreviewComponent
@@ -95,7 +94,6 @@ export default function MarkdownEditor({
setValue={setValue} setValue={setValue}
value={value} value={value}
maxHeight={maxHeight} maxHeight={maxHeight}
refreshDepArr={[refresh]}
{...editorProps} {...editorProps}
/> />
)} )}
@@ -1,4 +1,4 @@
import React, { ComponentProps } from "react"; import React, { type ComponentProps } from "react";
import AceEditor from "../../editors/AceEditor"; import AceEditor from "../../editors/AceEditor";
type Props = ComponentProps<typeof 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"; import { twMerge } from "tailwind-merge";
// @ts-ignore
import { serialize } from "next-mdx-remote/serialize"; import { serialize } from "next-mdx-remote/serialize";
// @ts-ignore
import remarkGfm from "remark-gfm"; import remarkGfm from "remark-gfm";
// @ts-ignore
import rehypePrismPlus from "rehype-prism-plus"; import rehypePrismPlus from "rehype-prism-plus";
// @ts-ignore
import { MDXRemote, MDXRemoteSerializeResult } from "next-mdx-remote"; import { MDXRemote, MDXRemoteSerializeResult } from "next-mdx-remote";
import Border from "../../elements/Border"; import Border from "../../elements/Border";
import { useMDXComponents } from "../mdx-components"; import { useMDXComponents } from "../mdx-components";
@@ -45,9 +49,11 @@ export default function MarkdownEditorPreviewComponent({
}, },
// scope: data, // scope: data,
}) })
// @ts-ignore
.then((mdxSrc) => { .then((mdxSrc) => {
setMdxSource(mdxSrc); setMdxSource(mdxSrc);
}) })
// @ts-ignore
.catch((err) => {}); .catch((err) => {});
} catch (error) {} } catch (error) {}
}, [value]); }, [value]);
@@ -58,7 +64,7 @@ export default function MarkdownEditorPreviewComponent({
className={twMerge( className={twMerge(
`w-full max-h-[${maxHeight}] h-[${maxHeight}] block px-6 pb-10`, `w-full max-h-[${maxHeight}] h-[${maxHeight}] block px-6 pb-10`,
"overflow-auto", "overflow-auto",
wrapperProps?.className wrapperProps?.className,
)} )}
> >
{mdxSource ? ( {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 CodeBlock from "../elements/CodeBlock";
import H1 from "../layout/H1"; import H1 from "../layout/H1";
import H4 from "../layout/H4"; import H4 from "../layout/H4";
+73
View File
@@ -1,3 +1,5 @@
import type { Ace } from "ace-builds";
export const AutocompleteOptions = [ export const AutocompleteOptions = [
// Personal Information // Personal Information
"name", "name",
@@ -66,3 +68,74 @@ export const AutocompleteOptions = [
"on", // Enables autofill (default) "on", // Enables autofill (default)
"off", // Disables autofill "off", // Disables autofill
] as const; ] 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 imageName = finalFile.name.replace(/\..*/, "");
let imageDataBase64: string | undefined;
const MIME_TYPE = mimeType ? mimeType : finalFile.type; const MIME_TYPE = mimeType ? mimeType : finalFile.type;
const QUALITY = 0.95; const QUALITY = 0.95;
const MAX_WIDTH = maxWidth ? maxWidth : null; const MAX_WIDTH = maxWidth ? maxWidth : null;
@@ -38,10 +36,11 @@ export default async function imageInputToBase64({
img.src = blobURL; img.src = blobURL;
imageDataBase64 = await new Promise((res, rej) => { const imageDataBase64 = await new Promise<string>((res, rej) => {
img.onerror = function () { img.onerror = function () {
URL.revokeObjectURL(this.src); URL.revokeObjectURL(this.src);
window.alert("Cannot load image!"); window.alert("Cannot load image!");
rej(new Error("Cannot load image"));
}; };
img.onload = function () { img.onload = function () {
@@ -72,7 +71,7 @@ export default async function imageInputToBase64({
res(srcEncoded); res(srcEncoded);
}; };
}); }).catch(() => undefined);
return { return {
imageBase64: imageDataBase64?.replace(/.*?base64,/, ""), imageBase64: imageDataBase64?.replace(/.*?base64,/, ""),
@@ -89,4 +88,4 @@ export default async function imageInputToBase64({
imageName: undefined, 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