From 8762e2da8d59e07e4b3a585a3e8e9825bb69861d Mon Sep 17 00:00:00 2001 From: Benjamin Toby Date: Thu, 27 Mar 2025 07:37:16 +0100 Subject: [PATCH] Updates --- .../lib/composites/docs/TWUIDocsAside.tsx | 38 ++++ .../lib/composites/docs/TWUIDocsLink.tsx | 121 +++++++++++++ components/lib/composites/docs/index.tsx | 76 ++++++++ components/lib/elements/Breadcrumbs.tsx | 79 ++++++--- components/lib/elements/CodeBlock.tsx | 151 ++++++++++++++++ components/lib/elements/Dropdown.tsx | 6 +- .../lib/elements/SingleLineCodeBlock.tsx | 24 +++ components/lib/elements/Toast.tsx | 28 ++- components/lib/form/FileUpload.tsx | 164 ++++++++++++++++++ components/lib/form/Input.tsx | 46 +++-- components/lib/form/Select.tsx | 2 +- .../lib/hooks/useCustomEventDispatch.tsx | 37 ++++ .../lib/hooks/useCustomEventListener.tsx | 39 +++++ components/lib/hooks/useLocalStorage.tsx | 31 ++++ components/lib/hooks/useWebSocket.tsx | 25 +-- components/lib/layout/Button.tsx | 16 +- components/lib/layout/Divider.tsx | 2 +- components/lib/layout/Link.tsx | 31 +++- components/lib/layout/Stack.tsx | 13 +- .../lib/next-js/hooks/useMDXComponents.tsx | 39 +++++ components/lib/package.json | 27 +++ .../lib/utils/form/fileInputToBase64.ts | 59 +++++++ layouts/main/(data)/links.tsx | 2 +- 23 files changed, 968 insertions(+), 88 deletions(-) create mode 100644 components/lib/composites/docs/TWUIDocsAside.tsx create mode 100644 components/lib/composites/docs/TWUIDocsLink.tsx create mode 100644 components/lib/composites/docs/index.tsx create mode 100644 components/lib/elements/CodeBlock.tsx create mode 100644 components/lib/elements/SingleLineCodeBlock.tsx create mode 100644 components/lib/form/FileUpload.tsx create mode 100644 components/lib/hooks/useCustomEventDispatch.tsx create mode 100644 components/lib/hooks/useCustomEventListener.tsx create mode 100644 components/lib/hooks/useLocalStorage.tsx create mode 100644 components/lib/next-js/hooks/useMDXComponents.tsx create mode 100644 components/lib/package.json create mode 100644 components/lib/utils/form/fileInputToBase64.ts diff --git a/components/lib/composites/docs/TWUIDocsAside.tsx b/components/lib/composites/docs/TWUIDocsAside.tsx new file mode 100644 index 0000000..3760489 --- /dev/null +++ b/components/lib/composites/docs/TWUIDocsAside.tsx @@ -0,0 +1,38 @@ +import { DetailedHTMLProps, HTMLAttributes } from "react"; +import { DocsLinkType } from "."; +import Stack from "../../layout/Stack"; +import TWUIDocsLink from "./TWUIDocsLink"; +import { twMerge } from "tailwind-merge"; + +type Props = DetailedHTMLProps, HTMLElement> & { + DocsLinks: DocsLinkType[]; + before?: React.ReactNode; + after?: React.ReactNode; + autoExpandAll?: boolean; +}; +export default function TWUIDocsAside({ + DocsLinks, + after, + before, + autoExpandAll, + ...props +}: Props) { + return ( + + ); +} diff --git a/components/lib/composites/docs/TWUIDocsLink.tsx b/components/lib/composites/docs/TWUIDocsLink.tsx new file mode 100644 index 0000000..3da24cd --- /dev/null +++ b/components/lib/composites/docs/TWUIDocsLink.tsx @@ -0,0 +1,121 @@ +import React, { + AnchorHTMLAttributes, + ComponentProps, + DetailedHTMLProps, +} from "react"; +import { DocsLinkType } from "."; +import Stack from "../../layout/Stack"; +import { twMerge } from "tailwind-merge"; +import Row from "../../layout/Row"; +import Divider from "../../layout/Divider"; +import { ChevronDown } from "lucide-react"; +import Button from "../../layout/Button"; + +type Props = DetailedHTMLProps< + AnchorHTMLAttributes, + HTMLAnchorElement +> & { + docLink: DocsLinkType; + wrapperProps?: ComponentProps; + strict?: boolean; + childWrapperProps?: ComponentProps; + autoExpandAll?: boolean; +}; + +/** + * # TWUI Docs Left Aside Link + * @note use dataset attribute `data-strict` for strict matching + * + * @className `twui-docs-left-aside-link` + */ +export default function TWUIDocsLink({ + docLink, + wrapperProps, + childWrapperProps, + strict, + autoExpandAll, + ...props +}: Props) { + const [isActive, setIsActive] = React.useState(false); + const [expand, setExpand] = React.useState(autoExpandAll || false); + const linkRef = React.useRef(null); + + React.useEffect(() => { + if (typeof window !== "undefined") { + const basePathMatch = window.location.pathname.includes( + docLink.href + ); + + const isStrictMatch = Boolean( + linkRef.current?.getAttribute("data-strict") + ); + + if (strict || isStrictMatch) { + setIsActive(window.location.pathname === docLink.href); + } else { + setIsActive(basePathMatch); + } + + if (basePathMatch) { + setExpand(true); + } + } + }, []); + + return ( + + + + {docLink.title} + + + {docLink.children?.[0] && ( + + )} + + {docLink.children && expand && ( + + + + {docLink.children.map((link, index) => ( + + ))} + + + )} + + ); +} diff --git a/components/lib/composites/docs/index.tsx b/components/lib/composites/docs/index.tsx new file mode 100644 index 0000000..c22b2b6 --- /dev/null +++ b/components/lib/composites/docs/index.tsx @@ -0,0 +1,76 @@ +import { + ComponentProps, + DetailedHTMLProps, + HTMLAttributes, + PropsWithChildren, +} from "react"; +import Stack from "../../layout/Stack"; +import Container from "../../layout/Container"; +import Row from "../../layout/Row"; +import Divider from "../../layout/Divider"; +import TWUIDocsAside from "./TWUIDocsAside"; +import { twMerge } from "tailwind-merge"; + +type Props = PropsWithChildren & { + DocsLinks: DocsLinkType[]; + docsAsideBefore?: React.ReactNode; + docsAsideAfter?: React.ReactNode; + wrapperProps?: ComponentProps; + docsContentProps?: ComponentProps; + leftAsideProps?: DetailedHTMLProps< + HTMLAttributes, + HTMLElement + >; + autoExpandAll?: boolean; +}; + +export type DocsLinkType = { + title: string; + href: string; + children?: DocsLinkType[]; +}; + +/** + * # TWUI Docs + * @className `twui-docs-content` + */ +export default function TWUIDocs({ + children, + DocsLinks, + docsAsideAfter, + docsAsideBefore, + wrapperProps, + docsContentProps, + leftAsideProps, + autoExpandAll, +}: Props) { + return ( + + + + + +
+ {children} +
+
+
+
+ ); +} diff --git a/components/lib/elements/Breadcrumbs.tsx b/components/lib/elements/Breadcrumbs.tsx index 2bd6084..c19bb2a 100644 --- a/components/lib/elements/Breadcrumbs.tsx +++ b/components/lib/elements/Breadcrumbs.tsx @@ -3,13 +3,24 @@ import Link from "../layout/Link"; import Divider from "../layout/Divider"; import Row from "../layout/Row"; import lowerToTitleCase from "../utils/lower-to-title-case"; +import { twMerge } from "tailwind-merge"; type LinkObject = { title: string; path: string; }; -export default function Breadcrumbs() { +type Props = { + excludeRegexMatch?: RegExp; +}; + +/** + * # TWUI Breadcrumbs + * @className `twui-current-breadcrumb-link` + * @className `twui-current-breadcrumb-wrapper` + */ +export default function Breadcrumbs({ excludeRegexMatch }: Props) { const [links, setLinks] = React.useState(null); + const [current, setCurrent] = React.useState(false); React.useEffect(() => { let pathname = window.location.pathname; @@ -27,6 +38,8 @@ export default function Breadcrumbs() { return; } + if (excludeRegexMatch && excludeRegexMatch.test(linkText)) return; + validPathLinks.push({ title: lowerToTitleCase(linkText), path: (() => { @@ -56,30 +69,50 @@ export default function Breadcrumbs() { } return ( - - {links.map((linkObject, index, array) => { - if (index === links.length - 1) { - return ( - - {linkObject.title} - - ); - } else { - return ( - - +
+ + {links.map((linkObject, index, array) => { + const isTarget = array.length - 1 == index; + + if (index === links.length - 1) { + return ( + {linkObject.title} - - - ); - } - })} - + ); + } else { + return ( + + + {linkObject.title} + + + + ); + } + })} + +
); //////////////////////////////////////// //////////////////////////////////////// diff --git a/components/lib/elements/CodeBlock.tsx b/components/lib/elements/CodeBlock.tsx new file mode 100644 index 0000000..3698718 --- /dev/null +++ b/components/lib/elements/CodeBlock.tsx @@ -0,0 +1,151 @@ +import { Check, Copy } from "lucide-react"; +import React, { + DetailedHTMLProps, + HTMLAttributes, + PropsWithChildren, +} from "react"; +import { twMerge } from "tailwind-merge"; +import Stack from "../layout/Stack"; +import Row from "../layout/Row"; +import Button from "../layout/Button"; +import Divider from "../layout/Divider"; + +export const TWUIPrismLanguages = ["shell", "javascript"] as const; + +type Props = PropsWithChildren & + DetailedHTMLProps, HTMLPreElement> & { + wrapperProps?: DetailedHTMLProps< + HTMLAttributes, + HTMLDivElement + >; + "data-title"?: string; + backgroundColor?: string; + singleBlock?: boolean; + language?: (typeof TWUIPrismLanguages)[number]; + }; + +/** + * # CodeBlock + * + * @className `twui-code-block-wrapper` + * @className `twui-code-pre-wrapper` + * @className `twui-code-block-pre` + * @className `twui-code-block-header` + */ +export default function CodeBlock({ + children, + wrapperProps, + backgroundColor, + singleBlock, + language, + ...props +}: Props) { + const codeRef = React.useRef(); + + const [copied, setCopied] = React.useState(false); + + const title = props?.["data-title"]; + + const finalBackgroundColor = backgroundColor || "#28272b"; + + return ( +
+ + + {title && {title}} +
+ {copied ? ( + + + Copied! + +
+ +
+
+ ) : ( +
+
+ {!singleBlock && ( + + )} +
+
+                        {children}
+                    
+
+
+
+ ); +} diff --git a/components/lib/elements/Dropdown.tsx b/components/lib/elements/Dropdown.tsx index a8530c4..5100c1e 100644 --- a/components/lib/elements/Dropdown.tsx +++ b/components/lib/elements/Dropdown.tsx @@ -37,6 +37,8 @@ let timeout: any; * @className_wrapper twui-dropdown-wrapper * @className_wrapper twui-dropdown-target * @className_wrapper twui-dropdown-content + * + * @note use the class `cancel-link` to prevent popup open on click */ export default function Dropdown({ contentWrapperProps, @@ -102,7 +104,9 @@ export default function Dropdown({ ref={dropdownRef} >
{ + onClick={(e) => { + const targetEl = e.target as HTMLElement | null; + if (targetEl?.closest?.(".cancel-link")) return; externalSetOpen?.(!open); setOpen(!open); }} diff --git a/components/lib/elements/SingleLineCodeBlock.tsx b/components/lib/elements/SingleLineCodeBlock.tsx new file mode 100644 index 0000000..b131457 --- /dev/null +++ b/components/lib/elements/SingleLineCodeBlock.tsx @@ -0,0 +1,24 @@ +import React, { DetailedHTMLProps, PropsWithChildren } from "react"; +import { twMerge } from "tailwind-merge"; + +type Props = PropsWithChildren & + DetailedHTMLProps, HTMLDivElement>; + +/** + * # Single Line CodeBlock + */ +export default function SingleLineCodeBlock({ children, ...props }: Props) { + return ( +
+ {children} +
+ ); +} diff --git a/components/lib/elements/Toast.tsx b/components/lib/elements/Toast.tsx index 1fb481b..2b0c2f6 100644 --- a/components/lib/elements/Toast.tsx +++ b/components/lib/elements/Toast.tsx @@ -8,7 +8,7 @@ import Span from "../layout/Span"; export const ToastStyles = ["normal", "success", "error"] as const; export const ToastColors = ToastStyles; -type Props = DetailedHTMLProps< +export type TWUIToastProps = DetailedHTMLProps< HTMLAttributes, HTMLDivElement > & { @@ -18,6 +18,9 @@ type Props = DetailedHTMLProps< color?: (typeof ToastStyles)[number]; }; +let interval: any; +let timeout: any; + /** * # Toast Component * @className twui-toast-root @@ -31,7 +34,7 @@ export default function Toast({ closeDelay = 4000, color, ...props -}: Props) { +}: TWUIToastProps) { if (!open) return null; const toastEl = ( @@ -47,10 +50,25 @@ export default function Toast({ props.className, "twui-toast" )} + onMouseEnter={() => { + window.clearTimeout(timeout); + }} + onMouseLeave={(e) => { + const targetEl = e.target as HTMLElement; + const rootWrapperEl = targetEl.closest( + ".twui-toast-root" + ) as HTMLDivElement | null; + + timeout = setTimeout(() => { + closeToast({ wrapperEl: rootWrapperEl }); + setOpen?.(false); + }, closeDelay); + }} > { const targetEl = e.target as HTMLElement; @@ -64,7 +82,7 @@ export default function Toast({ > - {props.children} + {props.children} ); @@ -81,7 +99,7 @@ export default function Toast({ const root = createRoot(wrapperEl); root.render(toastEl); - setTimeout(() => { + timeout = setTimeout(() => { closeToast({ wrapperEl }); setOpen?.(false); }, closeDelay); diff --git a/components/lib/form/FileUpload.tsx b/components/lib/form/FileUpload.tsx new file mode 100644 index 0000000..a6a0a95 --- /dev/null +++ b/components/lib/form/FileUpload.tsx @@ -0,0 +1,164 @@ +import Button from "../layout/Button"; +import Stack from "../layout/Stack"; +import { + File, + FileArchive, + FilePlus, + FilePlus2, + ImagePlus, + X, +} from "lucide-react"; +import React, { DetailedHTMLProps } from "react"; +import Card from "../elements/Card"; +import Span from "../layout/Span"; +import Center from "../layout/Center"; +import imageInputToBase64, { + FileInputToBase64FunctionReturn, +} from "../utils/form/fileInputToBase64"; +import { twMerge } from "tailwind-merge"; +import fileInputToBase64 from "../utils/form/fileInputToBase64"; +import Row from "../layout/Row"; + +type ImageUploadProps = DetailedHTMLProps< + React.HTMLAttributes, + HTMLDivElement +> & { + onChangeHandler?: ( + imgData: FileInputToBase64FunctionReturn | undefined + ) => any; + fileInputProps?: DetailedHTMLProps< + React.InputHTMLAttributes, + HTMLInputElement + >; + placeHolderWrapper?: DetailedHTMLProps< + React.HTMLAttributes, + HTMLDivElement + >; + previewImageWrapperProps?: DetailedHTMLProps< + React.HTMLAttributes, + HTMLDivElement + >; + previewImageProps?: DetailedHTMLProps< + React.ImgHTMLAttributes, + HTMLImageElement + >; + label?: string; + disablePreview?: boolean; + allowedRegex?: RegExp; + externalSetFile?: React.Dispatch< + React.SetStateAction + >; +}; + +/** + * @note use the `onChangeHandler` prop to grab the parsed base64 image object + */ +export default function FileUpload({ + onChangeHandler, + fileInputProps, + placeHolderWrapper, + previewImageWrapperProps, + previewImageProps, + label, + disablePreview, + allowedRegex, + externalSetFile, + ...props +}: ImageUploadProps) { + const [file, setFile] = React.useState< + FileInputToBase64FunctionReturn | undefined + >(undefined); + const inputRef = React.useRef(); + + return ( + + { + const inputFile = e.target.files?.[0]; + + if (!inputFile) return; + + fileInputToBase64({ inputFile, allowedRegex }).then( + (res) => { + setFile(res); + externalSetFile?.(res); + onChangeHandler?.(res); + fileInputProps?.onChange?.(e); + } + ); + }} + ref={inputRef as any} + /> + + {file ? ( + + {disablePreview ? ( + + Image Uploaded! + + ) : file.fileType?.match(/image/i) ? ( + + ) : ( + + + + {file.file?.name || file.fileName} + + {file.fileType} + + + + )} + + + ) : ( + { + inputRef.current?.click(); + placeHolderWrapper?.onClick?.(e); + }} + {...placeHolderWrapper} + > +
+ + + + {label || "Click to Upload File"} + + +
+
+ )} +
+ ); +} diff --git a/components/lib/form/Input.tsx b/components/lib/form/Input.tsx index 84b1516..9a5bf5a 100644 --- a/components/lib/form/Input.tsx +++ b/components/lib/form/Input.tsx @@ -135,15 +135,7 @@ export default function Input({ ...props }: InputProps) { const [focus, setFocus] = React.useState(false); - const [value, setValue] = React.useState( - props.value - ? String(props.value) - : props.defaultValue - ? String(props.defaultValue) - : "" - ); - - delete props.defaultValue; + const [value, setValue] = React.useState(props.defaultValue || props.value); const [isValid, setIsValid] = React.useState(true); @@ -151,27 +143,28 @@ export default function Input({ const finalDebounce = debounce || DEFAULT_DEBOUNCE; React.useEffect(() => { - if (!value.match(/./)) return setIsValid(true); - window.clearTimeout(timeout); + if (typeof value == "string") { + if (!value.match(/./)) return setIsValid(true); + window.clearTimeout(timeout); - if (validationRegex) { - timeout = setTimeout(() => { - setIsValid(validationRegex.test(value)); - }, finalDebounce); - } + if (validationRegex) { + timeout = setTimeout(() => { + setIsValid(validationRegex.test(value)); + }, finalDebounce); + } - if (validationFunction) { - timeout = setTimeout(() => { - validationFunction(value).then((res) => { - setIsValid(res); - }); - }, finalDebounce); + if (validationFunction) { + timeout = setTimeout(() => { + validationFunction(value).then((res) => { + setIsValid(res); + }); + }, finalDebounce); + } } }, [value]); React.useEffect(() => { - if (!props.value) return; - setValue(String(props.value)); + setValue(props.value || ""); }, [props.value]); const targetComponent = istextarea ? ( @@ -192,7 +185,10 @@ export default function Input({ props?.onBlur?.(e); }} value={value} - onChange={(e) => setValue(e.target.value)} + onChange={(e) => { + setValue(e.target.value); + props?.onChange?.(e); + }} autoComplete={autoComplete} rows={props.height ? Number(props.height) : 4} /> diff --git a/components/lib/form/Select.tsx b/components/lib/form/Select.tsx index efe7a2c..9283afc 100644 --- a/components/lib/form/Select.tsx +++ b/components/lib/form/Select.tsx @@ -89,7 +89,7 @@ export default function Select({ props.className )} ref={componentRef} - defaultValue={ + value={ options.flat().find((opt) => opt.default)?.value || undefined } diff --git a/components/lib/hooks/useCustomEventDispatch.tsx b/components/lib/hooks/useCustomEventDispatch.tsx new file mode 100644 index 0000000..65c53e8 --- /dev/null +++ b/components/lib/hooks/useCustomEventDispatch.tsx @@ -0,0 +1,37 @@ +import React from "react"; + +type Param = { + /** + * Custom Event Name + */ + name: string; +}; + +/** + * # Dispatch Custom Event + */ +export default function useCustomEventDispatch< + T extends { [key: string]: any } = { [key: string]: any } +>({ name }: Param) { + const dispatchCustomEvent = React.useCallback((value: T | string) => { + let dataParsed = typeof value == "object" ? value : undefined; + const str = typeof value == "string" ? value : undefined; + + if (str) { + try { + dataParsed = JSON.parse(str); + } catch (error) {} + } + + const event = new CustomEvent(name, { + detail: { + data: dataParsed, + message: str, + }, + }); + + window.dispatchEvent(event); + }, []); + + return { dispatchCustomEvent }; +} diff --git a/components/lib/hooks/useCustomEventListener.tsx b/components/lib/hooks/useCustomEventListener.tsx new file mode 100644 index 0000000..d4e73ef --- /dev/null +++ b/components/lib/hooks/useCustomEventListener.tsx @@ -0,0 +1,39 @@ +import React from "react"; + +type Param = { + /** + * Custom Event Name + */ + name: string; +}; + +/** + * # Listen For Custom Event + */ +export default function useCustomEventListener< + T extends { [key: string]: any } = { [key: string]: any } +>({ name }: Param) { + const [data, setData] = React.useState(undefined); + const [message, setMessage] = React.useState(undefined); + + const dataEventListenerCallback = React.useCallback((e: Event) => { + const customEvent = e as CustomEvent; + const eventPayloadMessage = customEvent.detail.message as + | string + | undefined; + const eventPayloadData = customEvent.detail.data as T | undefined; + + if (eventPayloadMessage) setMessage(eventPayloadMessage); + if (eventPayloadData) setData(eventPayloadData); + }, []); + + React.useEffect(() => { + window.addEventListener(name, dataEventListenerCallback, false); + + return function () { + window.removeEventListener(name, dataEventListenerCallback); + }; + }, []); + + return { data, message }; +} diff --git a/components/lib/hooks/useLocalStorage.tsx b/components/lib/hooks/useLocalStorage.tsx new file mode 100644 index 0000000..c04fb7f --- /dev/null +++ b/components/lib/hooks/useLocalStorage.tsx @@ -0,0 +1,31 @@ +import React from "react"; + +export type UseLocalStorageParam< + T extends { [key: string]: any } = { [key: string]: any } +> = { + key: keyof T; +}; + +/** + * # Use Local Storage + */ +export default function useLocalStorage< + T extends Record | undefined = undefined +>(param?: UseLocalStorageParam) { + const [data, setData] = + React.useState(); + + React.useEffect(() => { + if (param?.key) { + const value = localStorage.getItem(param.key as string); + try { + const jsonValue = JSON.parse(value || ""); + setData(jsonValue as any); + } catch (error) { + setData(value as any); + } + } + }, []); + + return { data }; +} diff --git a/components/lib/hooks/useWebSocket.tsx b/components/lib/hooks/useWebSocket.tsx index 102abc2..58c564d 100644 --- a/components/lib/hooks/useWebSocket.tsx +++ b/components/lib/hooks/useWebSocket.tsx @@ -10,10 +10,10 @@ let reconnectInterval: any; let msgInterval: any; let sendInterval: any; -let tries = 0; - export const WebSocketEventNames = ["wsDataEvent", "wsMessageEvent"] as const; +let tries = 0; + /** * # Use Websocket Hook * @event wsDataEvent Listen for event named `wsDataEvent` on `window` to receive Data events @@ -38,10 +38,6 @@ export default function useWebSocket< const messageQueueRef = React.useRef([]); const sendMessageQueueRef = React.useRef([]); - // const [message, setMessage] = React.useState(""); - // const [data, setData] = React.useState(null); - const [refresh, setRefresh] = React.useState(0); - const dispatchCustomEvent = React.useCallback( (evtName: (typeof WebSocketEventNames)[number], value: string | T) => { const event = new CustomEvent(evtName, { @@ -55,16 +51,17 @@ export default function useWebSocket< [] ); - React.useEffect(() => { + const connect = React.useCallback(() => { const wsURL = url; if (!wsURL) return; - const ws = new WebSocket(wsURL); + let ws = new WebSocket(wsURL); ws.onopen = (ev) => { window.clearInterval(reconnectInterval); setSocket(ws); tries = 0; + console.log(`Websocket connected to ${wsURL}`); }; ws.onmessage = (ev) => { @@ -83,16 +80,22 @@ export default function useWebSocket< if (tries >= 3) { return window.clearInterval(reconnectInterval); } + console.log("Attempting to reconnect ..."); - setRefresh(refresh + 1); tries++; + + connect(); }, 1000); }; + }, []); + + React.useEffect(() => { + connect(); return function () { window.clearInterval(reconnectInterval); }; - }, [refresh]); + }, []); /** * Received Message Queue Handler @@ -103,8 +106,6 @@ export default function useWebSocket< if (!newMessage) return; try { const jsonData = JSON.parse(newMessage); - // setData(jsonData); - dispatchCustomEvent("wsMessageEvent", newMessage); dispatchCustomEvent("wsDataEvent", jsonData); } catch (error) { diff --git a/components/lib/layout/Button.tsx b/components/lib/layout/Button.tsx index 3b84e2f..713fc4d 100644 --- a/components/lib/layout/Button.tsx +++ b/components/lib/layout/Button.tsx @@ -119,26 +119,26 @@ export default function Button({ } else if (variant == "ghost") { if (color == "primary" || !color) return twMerge( - "bg-transparent outline-none p-2", - "text-blue-500", + "bg-transparent dark:bg-transparent outline-none p-2", + "text-blue-500 hover:bg-transparent dark:hover:bg-transparent", "twui-button-primary-ghost" ); if (color == "secondary") return twMerge( - "bg-transparent outline-none p-2", - "text-emerald-500", + "bg-transparent dark:bg-transparent outline-none p-2", + "text-emerald-500 hover:bg-transparent dark:hover:bg-transparent", "twui-button-secondary-ghost" ); if (color == "accent") return twMerge( - "bg-transparent outline-none p-2", - "text-violet-500", + "bg-transparent dark:bg-transparent outline-none p-2", + "text-violet-500 hover:bg-transparent dark:hover:bg-transparent", "twui-button-accent-ghost" ); if (color == "gray") return twMerge( - "bg-transparent outline-none p-2", - "text-slate-600 dark:text-white/70", + "bg-transparent dark:bg-transparent outline-none p-2 hover:bg-transparent dark:hover:bg-transparent", + "text-slate-600 dark:text-white/70 hover:opacity-80", "twui-button-gray-ghost" ); if (color == "error") diff --git a/components/lib/layout/Divider.tsx b/components/lib/layout/Divider.tsx index 6efcf36..614754b 100644 --- a/components/lib/layout/Divider.tsx +++ b/components/lib/layout/Divider.tsx @@ -17,7 +17,7 @@ export default function Divider({
, + HTMLAnchorElement +> & { + showArrow?: boolean; + arrowSize?: number; + arrowProps?: Omit & RefAttributes; +}; /** * # General Anchor Elements * @className twui-a | twui-anchor */ export default function Link({ + showArrow, + arrowSize = 20, + arrowProps, ...props -}: DetailedHTMLProps< - AnchorHTMLAttributes, - HTMLAnchorElement ->) { +}: Props) { return ( {props.children} + {showArrow && ( + + )} ); } diff --git a/components/lib/layout/Stack.tsx b/components/lib/layout/Stack.tsx index b4b99e4..a6316a1 100644 --- a/components/lib/layout/Stack.tsx +++ b/components/lib/layout/Stack.tsx @@ -2,15 +2,18 @@ import _ from "lodash"; import { DetailedHTMLProps, HTMLAttributes } from "react"; import { twMerge } from "tailwind-merge"; +type Props = DetailedHTMLProps< + HTMLAttributes, + HTMLDivElement +> & { + center?: boolean; +}; + /** * # Flexbox Column * @className twui-stack */ -export default function Stack({ - ...props -}: DetailedHTMLProps, HTMLDivElement> & { - center?: boolean; -}) { +export default function Stack({ ...props }: Props) { const finalProps = _.omit(props, "center"); return (

{children}

, + h2: ({ children }) =>

{children}

, + h3: ({ children }) =>

{children}

, + h4: ({ children }) =>

{children}

, + pre: ({ children, ...props }) => { + if (React.isValidElement(children) && children.props) { + return ( + + {children.props.children} + + ); + } + return ( + + {children} + + ); + }, + ...components, + }; +} diff --git a/components/lib/package.json b/components/lib/package.json new file mode 100644 index 0000000..2d6f5e1 --- /dev/null +++ b/components/lib/package.json @@ -0,0 +1,27 @@ +{ + "name": "tailwind-ui", + "type": "module", + "dependencies": { + "@xterm/xterm": "^5.5.0", + "lodash": "^4.17.21", + "lucide-react": "^0.453.0", + "react": "^19.0.0", + "react-code-blocks": "^0.1.6", + "react-dom": "^19.0.0", + "react-responsive-modal": "^6.4.2", + "tailwind-merge": "^2.6.0", + "typescript": "^5.7.3" + }, + "devDependencies": { + "@types/ace": "^0.0.52", + "@types/bun": "latest", + "@types/lodash": "^4.17.15", + "@types/node": "^20.17.16", + "@types/react": "^18.3.18", + "@types/react-dom": "^18.3.5", + "postcss": "^8.5.1", + "tailwindcss": "^3.4.17", + "@types/mdx": "^2.0.13", + "@next/mdx": "^15.1.5" + } +} diff --git a/components/lib/utils/form/fileInputToBase64.ts b/components/lib/utils/form/fileInputToBase64.ts new file mode 100644 index 0000000..8a289e0 --- /dev/null +++ b/components/lib/utils/form/fileInputToBase64.ts @@ -0,0 +1,59 @@ +export type FileInputToBase64FunctionReturn = { + fileBase64?: string; + fileBase64Full?: string; + fileName?: string; + fileSize?: number; + fileType?: string; + file?: File; +}; + +export type FileInputToBase64FunctioParam = { + inputFile: File; + allowedRegex?: RegExp; +}; + +export default async function fileInputToBase64({ + inputFile, + allowedRegex, +}: FileInputToBase64FunctioParam): Promise { + const allowedTypesRegex = allowedRegex ? allowedRegex : undefined; + + if (allowedTypesRegex && !inputFile?.type?.match(allowedTypesRegex)) { + window.alert(`We currently don't support ${inputFile.type} file type.`); + return { fileName: inputFile.name }; + } + + let fileName = inputFile.name?.replace(/\..*/, ""); + const file = inputFile; + + try { + const fileData: string | undefined = await new Promise( + (resolve, reject) => { + var reader = new FileReader(); + reader.readAsDataURL(inputFile); + reader.onload = function () { + resolve(reader.result?.toString()); + }; + reader.onerror = function (/** @type {*} */ error: any) { + console.log("Error: ", error.message); + }; + } + ); + + return { + fileBase64: fileData?.replace(/.*?base64,/, ""), + fileBase64Full: fileData, + fileName: fileName, + fileSize: inputFile.size, + fileType: inputFile.type, + file, + }; + } catch (error: any) { + console.log("File Processing Error! =>", error.message); + + return { + fileName: inputFile.name, + file, + }; + } +} diff --git a/layouts/main/(data)/links.tsx b/layouts/main/(data)/links.tsx index 96a61de..39e3567 100644 --- a/layouts/main/(data)/links.tsx +++ b/layouts/main/(data)/links.tsx @@ -59,7 +59,7 @@ export const SocialLinks: SocialLinksType[] = [ }, { name: "Git", - href: "https://git.tben.me", + href: "https://git.tben.me/explore/repos", icon: , }, {