diff --git a/components/(partials)/ModalComponent.tsx b/components/(partials)/ModalComponent.tsx index 00c999d..4c5b31b 100644 --- a/components/(partials)/ModalComponent.tsx +++ b/components/(partials)/ModalComponent.tsx @@ -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(
{ setOpen(false); @@ -43,23 +48,25 @@ export default function ModalComponent({ open, setOpen, ...props }: Props) { "z-10 max-w-modal bg-background-light dark:bg-background-dark", "w-full relative max-h-[95vh] overflow-y-auto", "twui-modal-content", - props.className + props.className, )} > {props.children} - + {!no_cancel_button && ( + + )}
, - document.getElementById("twui-modal-root") as HTMLElement + document.getElementById("twui-modal-root") as HTMLElement, ); } diff --git a/components/(partials)/PopoverComponent.tsx b/components/(partials)/PopoverComponent.tsx index e75658d..9e04c07 100644 --- a/components/(partials)/PopoverComponent.tsx +++ b/components/(partials)/PopoverComponent.tsx @@ -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(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} , - document.getElementById("twui-popover-root") as HTMLElement + document.getElementById("twui-popover-root") as HTMLElement, ); } diff --git a/components/(utils)/ejson.ts b/components/(utils)/ejson.ts new file mode 100644 index 0000000..b4f0528 --- /dev/null +++ b/components/(utils)/ejson.ts @@ -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; diff --git a/components/(utils)/search-select/keyboard-ops.ts b/components/(utils)/search-select/keyboard-ops.ts new file mode 100644 index 0000000..e69de29 diff --git a/components/Readme.md b/components/Readme.md index 4f0d2ae..ac57f17 100644 --- a/components/Readme.md +++ b/components/Readme.md @@ -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 diff --git a/components/base.css b/components/base.css index 657b34e..1e311e3 100644 --- a/components/base.css +++ b/components/base.css @@ -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; diff --git a/components/composites/docs/TWUIDocsAside.tsx b/components/composites/docs/TWUIDocsAside.tsx index 134f1f6..5147a16 100644 --- a/components/composites/docs/TWUIDocsAside.tsx +++ b/components/composites/docs/TWUIDocsAside.tsx @@ -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"; diff --git a/components/composites/docs/TWUIDocsLink.tsx b/components/composites/docs/TWUIDocsLink.tsx index 5c62232..2a14d47 100644 --- a/components/composites/docs/TWUIDocsLink.tsx +++ b/components/composites/docs/TWUIDocsLink.tsx @@ -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({ diff --git a/components/composites/docs/TWUIDocsRightAside.tsx b/components/composites/docs/TWUIDocsRightAside.tsx index 531049d..6f46c4d 100644 --- a/components/composites/docs/TWUIDocsRightAside.tsx +++ b/components/composites/docs/TWUIDocsRightAside.tsx @@ -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, )} > diff --git a/components/composites/docs/index.tsx b/components/composites/docs/index.tsx index f3ab2c1..5eb645a 100644 --- a/components/composites/docs/index.tsx +++ b/components/composites/docs/index.tsx @@ -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, )} > {children} diff --git a/components/editors/AceEditor.tsx b/components/editors/AceEditor.tsx index 2b5033b..ea00194 100644 --- a/components/editors/AceEditor.tsx +++ b/components/editors/AceEditor.tsx @@ -1,172 +1,176 @@ -import React, { MutableRefObject } from "react"; -import { twMerge } from "tailwind-merge"; -import AceEditorModes from "./ace-editor-modes"; -import { AceEditorOptions } from "@moduletrace/datasquirel/dist/package-shared/types"; - -export type AceEditorComponentType = { - editorRef?: MutableRefObject; - readOnly?: boolean; - /** Function to call when Ctrl+Enter is pressed */ - ctrlEnterFn?: (editor: AceAjax.Editor) => void; - content?: string; - placeholder?: string; - title?: string; - mode?: (typeof AceEditorModes)[number]; - fontSize?: string; - previewMode?: boolean; - onChange?: (value: string) => void; - delay?: number; - wrapperProps?: React.DetailedHTMLProps< - React.HTMLAttributes, - HTMLDivElement - >; - refreshDepArr?: any[]; - editorOptions?: AceEditorOptions; - showLabel?: boolean; -}; - -let timeout: any; - -/** - * # Powerful Ace Editor - * @note **NOTE** head scripts required - * @script `https://cdnjs.cloudflare.com/ajax/libs/ace/1.22.0/ace.min.js` - * @script `https://cdnjs.cloudflare.com/ajax/libs/ace/1.22.0/ext-language_tools.min.js` - */ -export default function AceEditor({ - editorRef, - readOnly, - ctrlEnterFn, - content = "", - placeholder, - mode, - fontSize, - previewMode, - onChange, - delay = 500, - refreshDepArr, - wrapperProps, - editorOptions, - showLabel, - title, -}: AceEditorComponentType) { - try { - const editorElementRef = React.useRef(undefined); - const editorRefInstance = React.useRef(undefined); - - const [refresh, setRefresh] = React.useState(0); - const [darkMode, setDarkMode] = React.useState(false); - const [ready, setReady] = React.useState(false); - - React.useEffect(() => { - if (!ready) return; - - if (!ace?.edit || !editorElementRef.current) { - setTimeout(() => { - setRefresh((prev) => prev + 1); - }, 1000); - return; - } - - const editor = ace.edit(editorElementRef.current); - - editor.setOptions({ - mode: `ace/mode/${mode ? mode : "javascript"}`, - theme: darkMode - ? "ace/theme/tomorrow_night_eighties" - : "ace/theme/ace_light", - value: (() => { - try { - return JSON.stringify(JSON.parse(content), null, 4); - } catch (error) { - return content; - } - })(), - placeholder: placeholder ? placeholder : "", - enableBasicAutocompletion: true, - enableLiveAutocompletion: true, - readOnly: readOnly ? true : false, - fontSize: fontSize ? fontSize : null, - showLineNumbers: previewMode ? false : true, - wrap: true, - wrapMethod: "code", - ...editorOptions, - }); - - editor.commands.addCommand({ - name: "myCommand", - bindKey: { win: "Ctrl-Enter", mac: "Command-Enter" }, - exec: function (editor) { - if (ctrlEnterFn) ctrlEnterFn(editor); - }, - readOnly: true, - }); - - editor.getSession().on("change", function (e) { - if (onChange) { - clearTimeout(timeout); - - setTimeout(() => { - try { - onChange(editor.getValue()); - } catch (error) {} - }, delay); - } - }); - - editorRefInstance.current = editor; - if (editorRef) editorRef.current = editor; - - return function () { - editor.destroy(); - }; - }, [refresh, darkMode, ready, mode, ...(refreshDepArr || [])]); - - React.useEffect(() => { - const htmlClassName = document.documentElement.className; - if (htmlClassName.match(/dark/i)) setDarkMode(true); - setTimeout(() => { - setReady(true); - }, 200); - }, []); - - return ( - -
- {showLabel && title ? ( - - ) : null} -
-
-
- ); - } catch (error: any) { - return ( - - - Editor Error:{" "} - {error.message} - - - ); - } -} +import React, { type RefObject } from "react"; +import { twMerge } from "tailwind-merge"; +import AceEditorModes from "./ace-editor-modes"; +import type { Ace as AceAjax } from "ace-builds"; + +export type AceEditorComponentType = { + editorRef?: RefObject; + readOnly?: boolean; + /** Function to call when Ctrl+Enter is pressed */ + ctrlEnterFn?: (editor: AceAjax.Editor) => void; + content?: string; + placeholder?: string; + title?: string; + mode?: (typeof AceEditorModes)[number]; + fontSize?: string; + previewMode?: boolean; + onChange?: (value: string, editor?: AceAjax.Editor) => void; + delay?: number; + wrapperProps?: React.DetailedHTMLProps< + React.HTMLAttributes, + HTMLDivElement + >; + editorOptions?: AceAjax.EditorOptions; + showLabel?: boolean; +}; + +let timeout: any; + +/** + * # Powerful Ace Editor + * @note **NOTE** head scripts required + * @script `https://cdnjs.cloudflare.com/ajax/libs/ace/1.22.0/ace.min.js` + * @script `https://cdnjs.cloudflare.com/ajax/libs/ace/1.22.0/ext-language_tools.min.js` + */ +export default function AceEditor({ + editorRef, + readOnly, + ctrlEnterFn, + content = "", + placeholder, + mode, + fontSize, + previewMode, + onChange, + delay = 500, + wrapperProps, + editorOptions, + showLabel, + title, +}: AceEditorComponentType) { + try { + const editorElementRef = React.useRef(undefined); + const editorRefInstance = React.useRef(undefined); + + const [refresh, setRefresh] = React.useState(0); + const [darkMode, setDarkMode] = React.useState(false); + const [ready, setReady] = React.useState(false); + + React.useEffect(() => { + if (!ready) return; + + // @ts-ignore + const ace = window.ace; + + if (!ace?.edit || !editorElementRef.current) { + setTimeout(() => { + setRefresh((prev) => prev + 1); + }, 1000); + return; + } + + // @ts-ignore + const editor = ace.edit(editorElementRef.current) as AceAjax.Editor; + + editor.setOptions({ + mode: `ace/mode/${mode ? mode : "javascript"}`, + theme: darkMode + ? "ace/theme/tomorrow_night_eighties" + : "ace/theme/ace_light", + value: (() => { + try { + return JSON.stringify(JSON.parse(content), null, 4); + } catch (error) { + return content; + } + })(), + placeholder: placeholder ? placeholder : "", + enableBasicAutocompletion: true, + enableLiveAutocompletion: true, + readOnly: readOnly ? true : false, + fontSize: fontSize ? fontSize : undefined, + showLineNumbers: previewMode ? false : true, + wrap: true, + wrapMethod: "code", + ...editorOptions, + }); + + editor.commands.addCommand({ + name: "myCommand", + bindKey: { win: "Ctrl-Enter", mac: "Command-Enter" }, + exec: function (editor) { + if (ctrlEnterFn) ctrlEnterFn(editor); + }, + readOnly: true, + }); + + editor.getSession().on("change", function (e) { + if (onChange) { + clearTimeout(timeout); + + setTimeout(() => { + try { + onChange( + editor.getValue(), + editorRefInstance.current, + ); + } catch (error) {} + }, delay); + } + }); + + editorRefInstance.current = editor; + if (editorRef) editorRef.current = editor; + + return function () { + editor.destroy(); + }; + }, [refresh, darkMode, ready, mode]); + + React.useEffect(() => { + const htmlClassName = document.documentElement.className; + if (htmlClassName.match(/dark/i)) setDarkMode(true); + setTimeout(() => { + setReady(true); + }, 200); + }, []); + + return ( + +
+ {showLabel && title ? ( + + ) : null} +
+
+
+ ); + } catch (error: any) { + return ( + + + Editor Error: {error.message} + + + ); + } +} diff --git a/components/editors/TinyMCE/index.tsx b/components/editors/TinyMCE/index.tsx index 6884487..fe20a0e 100644 --- a/components/editors/TinyMCE/index.tsx +++ b/components/editors/TinyMCE/index.tsx @@ -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 = { options?: RawEditorOptions; - editorRef?: React.MutableRefObject; + editorRef?: React.RefObject; setEditor?: React.Dispatch>; wrapperProps?: React.DetailedHTMLProps< React.HTMLAttributes, @@ -20,11 +20,12 @@ export type TinyMCEEditorProps = { borderProps?: ComponentProps; 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({ useParentCSS, placeholder, refreshDependencyArray, + base_url, }: TinyMCEEditorProps) { - const { tinyMCE } = useTinyMCE(); + const { tinyMCE } = useTinyMCE({ base_url }); const editorComponentRef = React.useRef(null); - const editorRef: MutableRefObject = + const editorRef: RefObject = passedEditorRef || React.useRef(null); const EDITOR_VALUE_CHANGE_TIMEOUT = 500; @@ -59,7 +61,6 @@ export default function TinyMCEEditor({ const [ready, setReady] = React.useState(false); const [darkMode, setDarkMode] = React.useState(false); const [refresh, setRefresh] = React.useState(0); - const [editor, setEditor] = React.useState(); const title = name ? twuiSlugToNormalText(name) : "Rich Text"; @@ -81,13 +82,28 @@ export default function TinyMCEEditor({ 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({ 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({ 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({ }; }, [tinyMCE, themeReady, refresh, ...(refreshDependencyArray || [])]); - React.useEffect(() => { - const instance = editorRef.current; - - if (instance) { - instance.setContent(defaultValue || ""); - } - }, [defaultValue]); - return (
({ "bg-background-light dark:bg-background-dark", wrapperWrapperProps?.className, )} - onInput={(e) => { - console.log(`Input Detected`); - }} > {showLabel && (