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