Updates
This commit is contained in:
parent
9dd6c3a70e
commit
8762e2da8d
38
components/lib/composites/docs/TWUIDocsAside.tsx
Normal file
38
components/lib/composites/docs/TWUIDocsAside.tsx
Normal file
@ -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<HTMLAttributes<HTMLElement>, HTMLElement> & {
|
||||||
|
DocsLinks: DocsLinkType[];
|
||||||
|
before?: React.ReactNode;
|
||||||
|
after?: React.ReactNode;
|
||||||
|
autoExpandAll?: boolean;
|
||||||
|
};
|
||||||
|
export default function TWUIDocsAside({
|
||||||
|
DocsLinks,
|
||||||
|
after,
|
||||||
|
before,
|
||||||
|
autoExpandAll,
|
||||||
|
...props
|
||||||
|
}: Props) {
|
||||||
|
return (
|
||||||
|
<aside
|
||||||
|
{...props}
|
||||||
|
className={twMerge("py-10 hidden xl:flex", props.className)}
|
||||||
|
>
|
||||||
|
<Stack>
|
||||||
|
{before}
|
||||||
|
{DocsLinks.map((link, index) => (
|
||||||
|
<TWUIDocsLink
|
||||||
|
docLink={link}
|
||||||
|
key={index}
|
||||||
|
autoExpandAll={autoExpandAll}
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
{after}
|
||||||
|
</Stack>
|
||||||
|
</aside>
|
||||||
|
);
|
||||||
|
}
|
121
components/lib/composites/docs/TWUIDocsLink.tsx
Normal file
121
components/lib/composites/docs/TWUIDocsLink.tsx
Normal file
@ -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>,
|
||||||
|
HTMLAnchorElement
|
||||||
|
> & {
|
||||||
|
docLink: DocsLinkType;
|
||||||
|
wrapperProps?: ComponentProps<typeof Stack>;
|
||||||
|
strict?: boolean;
|
||||||
|
childWrapperProps?: ComponentProps<typeof Stack>;
|
||||||
|
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<HTMLAnchorElement>(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 (
|
||||||
|
<Stack
|
||||||
|
className={twMerge("gap-2 w-full", wrapperProps?.className)}
|
||||||
|
{...wrapperProps}
|
||||||
|
>
|
||||||
|
<Row className="flex-nowrap grow justify-between w-full">
|
||||||
|
<a
|
||||||
|
href={docLink.href}
|
||||||
|
{...props}
|
||||||
|
className={twMerge(
|
||||||
|
"twui-docs-left-aside-link whitespace-nowrap",
|
||||||
|
"grow",
|
||||||
|
isActive ? "active" : "",
|
||||||
|
props.className
|
||||||
|
)}
|
||||||
|
ref={linkRef}
|
||||||
|
>
|
||||||
|
{docLink.title}
|
||||||
|
</a>
|
||||||
|
|
||||||
|
{docLink.children?.[0] && (
|
||||||
|
<Button
|
||||||
|
variant="ghost"
|
||||||
|
color="gray"
|
||||||
|
className={twMerge(
|
||||||
|
"p-1 hover:opacity-100",
|
||||||
|
expand ? "rotate-180 opacity-30" : "opacity-70"
|
||||||
|
)}
|
||||||
|
onClick={() => setExpand(!expand)}
|
||||||
|
>
|
||||||
|
<ChevronDown className="text-slate-500" size={20} />
|
||||||
|
</Button>
|
||||||
|
)}
|
||||||
|
</Row>
|
||||||
|
{docLink.children && expand && (
|
||||||
|
<Row className="items-stretch gap-4 grow w-full flex-nowrap">
|
||||||
|
<Divider vertical className="h-auto" />
|
||||||
|
<Stack
|
||||||
|
className={twMerge(
|
||||||
|
"gap-2 w-full",
|
||||||
|
childWrapperProps?.className
|
||||||
|
)}
|
||||||
|
{...childWrapperProps}
|
||||||
|
>
|
||||||
|
{docLink.children.map((link, index) => (
|
||||||
|
<TWUIDocsLink
|
||||||
|
docLink={link}
|
||||||
|
key={index}
|
||||||
|
className="text-sm"
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
</Stack>
|
||||||
|
</Row>
|
||||||
|
)}
|
||||||
|
</Stack>
|
||||||
|
);
|
||||||
|
}
|
76
components/lib/composites/docs/index.tsx
Normal file
76
components/lib/composites/docs/index.tsx
Normal file
@ -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<typeof Stack>;
|
||||||
|
docsContentProps?: ComponentProps<typeof Row>;
|
||||||
|
leftAsideProps?: DetailedHTMLProps<
|
||||||
|
HTMLAttributes<HTMLElement>,
|
||||||
|
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 (
|
||||||
|
<Stack
|
||||||
|
center
|
||||||
|
{...wrapperProps}
|
||||||
|
className={twMerge("w-full px-4 sm:px-6", wrapperProps?.className)}
|
||||||
|
>
|
||||||
|
<Container>
|
||||||
|
<Row
|
||||||
|
{...docsContentProps}
|
||||||
|
className={twMerge(
|
||||||
|
"items-stretch gap-6 w-full flex-nowrap",
|
||||||
|
docsContentProps?.className
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
<TWUIDocsAside
|
||||||
|
DocsLinks={DocsLinks}
|
||||||
|
after={docsAsideAfter}
|
||||||
|
before={docsAsideBefore}
|
||||||
|
autoExpandAll={autoExpandAll}
|
||||||
|
{...leftAsideProps}
|
||||||
|
/>
|
||||||
|
<Divider vertical className="h-auto hidden xl:flex" />
|
||||||
|
<div className="block twui-docs-content py-10 pl-0 xl:pl-6 grow">
|
||||||
|
{children}
|
||||||
|
</div>
|
||||||
|
</Row>
|
||||||
|
</Container>
|
||||||
|
</Stack>
|
||||||
|
);
|
||||||
|
}
|
@ -3,13 +3,24 @@ import Link from "../layout/Link";
|
|||||||
import Divider from "../layout/Divider";
|
import Divider from "../layout/Divider";
|
||||||
import Row from "../layout/Row";
|
import Row from "../layout/Row";
|
||||||
import lowerToTitleCase from "../utils/lower-to-title-case";
|
import lowerToTitleCase from "../utils/lower-to-title-case";
|
||||||
|
import { twMerge } from "tailwind-merge";
|
||||||
|
|
||||||
type LinkObject = {
|
type LinkObject = {
|
||||||
title: string;
|
title: string;
|
||||||
path: 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<LinkObject[] | null>(null);
|
const [links, setLinks] = React.useState<LinkObject[] | null>(null);
|
||||||
|
const [current, setCurrent] = React.useState(false);
|
||||||
|
|
||||||
React.useEffect(() => {
|
React.useEffect(() => {
|
||||||
let pathname = window.location.pathname;
|
let pathname = window.location.pathname;
|
||||||
@ -27,6 +38,8 @@ export default function Breadcrumbs() {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (excludeRegexMatch && excludeRegexMatch.test(linkText)) return;
|
||||||
|
|
||||||
validPathLinks.push({
|
validPathLinks.push({
|
||||||
title: lowerToTitleCase(linkText),
|
title: lowerToTitleCase(linkText),
|
||||||
path: (() => {
|
path: (() => {
|
||||||
@ -56,30 +69,50 @@ export default function Breadcrumbs() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Row className="gap-4 flex-nowrap whitespace-nowrap overflow-x-auto w-full">
|
<div
|
||||||
{links.map((linkObject, index, array) => {
|
className={twMerge(
|
||||||
if (index === links.length - 1) {
|
"overflow-x-auto max-w-[70vw]",
|
||||||
return (
|
"twui-current-breadcrumb-wrapper"
|
||||||
<Link
|
)}
|
||||||
key={index}
|
>
|
||||||
href={linkObject.path}
|
<Row className="gap-4 flex-nowrap whitespace-nowrap overflow-x-auto w-full">
|
||||||
className="text-slate-400 dark:text-slate-500 pointer-events-none text-xs"
|
{links.map((linkObject, index, array) => {
|
||||||
>
|
const isTarget = array.length - 1 == index;
|
||||||
{linkObject.title}
|
|
||||||
</Link>
|
if (index === links.length - 1) {
|
||||||
);
|
return (
|
||||||
} else {
|
<Link
|
||||||
return (
|
key={index}
|
||||||
<React.Fragment key={index}>
|
href={linkObject.path}
|
||||||
<Link href={linkObject.path} className="text-xs">
|
className={twMerge(
|
||||||
|
"text-slate-400 dark:text-slate-500 pointer-events-none text-xs",
|
||||||
|
isTarget ? "current" : "",
|
||||||
|
"twui-current-breadcrumb-link"
|
||||||
|
)}
|
||||||
|
>
|
||||||
{linkObject.title}
|
{linkObject.title}
|
||||||
</Link>
|
</Link>
|
||||||
<Divider vertical />
|
);
|
||||||
</React.Fragment>
|
} else {
|
||||||
);
|
return (
|
||||||
}
|
<React.Fragment key={index}>
|
||||||
})}
|
<Link
|
||||||
</Row>
|
href={linkObject.path}
|
||||||
|
className={twMerge(
|
||||||
|
"text-xs",
|
||||||
|
isTarget ? "current" : "",
|
||||||
|
"twui-current-breadcrumb-link"
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
{linkObject.title}
|
||||||
|
</Link>
|
||||||
|
<Divider vertical />
|
||||||
|
</React.Fragment>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
})}
|
||||||
|
</Row>
|
||||||
|
</div>
|
||||||
);
|
);
|
||||||
////////////////////////////////////////
|
////////////////////////////////////////
|
||||||
////////////////////////////////////////
|
////////////////////////////////////////
|
||||||
|
151
components/lib/elements/CodeBlock.tsx
Normal file
151
components/lib/elements/CodeBlock.tsx
Normal file
@ -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<HTMLAttributes<HTMLPreElement>, HTMLPreElement> & {
|
||||||
|
wrapperProps?: DetailedHTMLProps<
|
||||||
|
HTMLAttributes<HTMLDivElement>,
|
||||||
|
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<HTMLDivElement>();
|
||||||
|
|
||||||
|
const [copied, setCopied] = React.useState(false);
|
||||||
|
|
||||||
|
const title = props?.["data-title"];
|
||||||
|
|
||||||
|
const finalBackgroundColor = backgroundColor || "#28272b";
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
{...wrapperProps}
|
||||||
|
className={twMerge(
|
||||||
|
"outline outline-[1px] outline-slate-200 dark:outline-white/10",
|
||||||
|
`rounded w-full transition-all items-start`,
|
||||||
|
"relative",
|
||||||
|
"twui-code-block-wrapper",
|
||||||
|
wrapperProps?.className
|
||||||
|
)}
|
||||||
|
style={{
|
||||||
|
boxShadow: copied
|
||||||
|
? "0 0 10px 10px rgba(18, 139, 99, 0.2)"
|
||||||
|
: undefined,
|
||||||
|
maxWidth: "calc(100vw - 80px)",
|
||||||
|
backgroundColor: finalBackgroundColor,
|
||||||
|
...props.style,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Stack
|
||||||
|
className={twMerge(
|
||||||
|
"gap-0 w-full overflow-x-auto relative",
|
||||||
|
"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"
|
||||||
|
)}
|
||||||
|
style={{
|
||||||
|
backgroundColor: finalBackgroundColor,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{title && <span className="text-white/70">{title}</span>}
|
||||||
|
<div className="ml-auto">
|
||||||
|
{copied ? (
|
||||||
|
<Row>
|
||||||
|
<span className="text-white text-xs twui-code-block-copied-text">
|
||||||
|
Copied!
|
||||||
|
</span>
|
||||||
|
<div className="w-5 h-5 rounded-full bg-emerald-600 text-white flex items-center justify-center">
|
||||||
|
<Check size={15} />
|
||||||
|
</div>
|
||||||
|
</Row>
|
||||||
|
) : (
|
||||||
|
<Button
|
||||||
|
variant="ghost"
|
||||||
|
color="gray"
|
||||||
|
beforeIcon={<Copy size={17} color="white" />}
|
||||||
|
className="!p-1 !bg-transparent"
|
||||||
|
onClick={() => {
|
||||||
|
const content =
|
||||||
|
codeRef.current?.textContent;
|
||||||
|
if (!content) {
|
||||||
|
window.alert("No Content to copy");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
window.navigator.clipboard
|
||||||
|
.writeText(content)
|
||||||
|
.then(() => {
|
||||||
|
setCopied(true);
|
||||||
|
setTimeout(() => {
|
||||||
|
setCopied(false);
|
||||||
|
}, 3000);
|
||||||
|
});
|
||||||
|
}}
|
||||||
|
title="Copy Code Snippet"
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</Row>
|
||||||
|
{!singleBlock && (
|
||||||
|
<Divider className="!border-white/10 sticky top-10" />
|
||||||
|
)}
|
||||||
|
<div
|
||||||
|
className={twMerge(
|
||||||
|
`p-1 w-full [&_pre]:!bg-transparent`,
|
||||||
|
singleBlock ? "" : "-mt-1",
|
||||||
|
"twui-code-pre-wrapper"
|
||||||
|
)}
|
||||||
|
ref={codeRef as any}
|
||||||
|
>
|
||||||
|
<pre
|
||||||
|
{...props}
|
||||||
|
className={twMerge(
|
||||||
|
"!my-0",
|
||||||
|
language ? `language-${language}` : "",
|
||||||
|
"twui-code-block-pre",
|
||||||
|
props.className
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
{children}
|
||||||
|
</pre>
|
||||||
|
</div>
|
||||||
|
</Stack>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
@ -37,6 +37,8 @@ let timeout: any;
|
|||||||
* @className_wrapper twui-dropdown-wrapper
|
* @className_wrapper twui-dropdown-wrapper
|
||||||
* @className_wrapper twui-dropdown-target
|
* @className_wrapper twui-dropdown-target
|
||||||
* @className_wrapper twui-dropdown-content
|
* @className_wrapper twui-dropdown-content
|
||||||
|
*
|
||||||
|
* @note use the class `cancel-link` to prevent popup open on click
|
||||||
*/
|
*/
|
||||||
export default function Dropdown({
|
export default function Dropdown({
|
||||||
contentWrapperProps,
|
contentWrapperProps,
|
||||||
@ -102,7 +104,9 @@ export default function Dropdown({
|
|||||||
ref={dropdownRef}
|
ref={dropdownRef}
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
onClick={() => {
|
onClick={(e) => {
|
||||||
|
const targetEl = e.target as HTMLElement | null;
|
||||||
|
if (targetEl?.closest?.(".cancel-link")) return;
|
||||||
externalSetOpen?.(!open);
|
externalSetOpen?.(!open);
|
||||||
setOpen(!open);
|
setOpen(!open);
|
||||||
}}
|
}}
|
||||||
|
24
components/lib/elements/SingleLineCodeBlock.tsx
Normal file
24
components/lib/elements/SingleLineCodeBlock.tsx
Normal file
@ -0,0 +1,24 @@
|
|||||||
|
import React, { DetailedHTMLProps, PropsWithChildren } from "react";
|
||||||
|
import { twMerge } from "tailwind-merge";
|
||||||
|
|
||||||
|
type Props = PropsWithChildren &
|
||||||
|
DetailedHTMLProps<React.HTMLAttributes<HTMLDivElement>, HTMLDivElement>;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* # Single Line CodeBlock
|
||||||
|
*/
|
||||||
|
export default function SingleLineCodeBlock({ children, ...props }: Props) {
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
{...props}
|
||||||
|
className={twMerge(
|
||||||
|
"[&_.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
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
{children}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
@ -8,7 +8,7 @@ import Span from "../layout/Span";
|
|||||||
export const ToastStyles = ["normal", "success", "error"] as const;
|
export const ToastStyles = ["normal", "success", "error"] as const;
|
||||||
export const ToastColors = ToastStyles;
|
export const ToastColors = ToastStyles;
|
||||||
|
|
||||||
type Props = DetailedHTMLProps<
|
export type TWUIToastProps = DetailedHTMLProps<
|
||||||
HTMLAttributes<HTMLDivElement>,
|
HTMLAttributes<HTMLDivElement>,
|
||||||
HTMLDivElement
|
HTMLDivElement
|
||||||
> & {
|
> & {
|
||||||
@ -18,6 +18,9 @@ type Props = DetailedHTMLProps<
|
|||||||
color?: (typeof ToastStyles)[number];
|
color?: (typeof ToastStyles)[number];
|
||||||
};
|
};
|
||||||
|
|
||||||
|
let interval: any;
|
||||||
|
let timeout: any;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* # Toast Component
|
* # Toast Component
|
||||||
* @className twui-toast-root
|
* @className twui-toast-root
|
||||||
@ -31,7 +34,7 @@ export default function Toast({
|
|||||||
closeDelay = 4000,
|
closeDelay = 4000,
|
||||||
color,
|
color,
|
||||||
...props
|
...props
|
||||||
}: Props) {
|
}: TWUIToastProps) {
|
||||||
if (!open) return null;
|
if (!open) return null;
|
||||||
|
|
||||||
const toastEl = (
|
const toastEl = (
|
||||||
@ -47,10 +50,25 @@ export default function Toast({
|
|||||||
props.className,
|
props.className,
|
||||||
"twui-toast"
|
"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);
|
||||||
|
}}
|
||||||
>
|
>
|
||||||
<Span
|
<Span
|
||||||
className={twMerge(
|
className={twMerge(
|
||||||
"absolute top-2 right-2 z-[100] cursor-pointer"
|
"absolute top-2 right-2 z-[100] cursor-pointer",
|
||||||
|
"text-white"
|
||||||
)}
|
)}
|
||||||
onClick={(e) => {
|
onClick={(e) => {
|
||||||
const targetEl = e.target as HTMLElement;
|
const targetEl = e.target as HTMLElement;
|
||||||
@ -64,7 +82,7 @@ export default function Toast({
|
|||||||
>
|
>
|
||||||
<X size={15} />
|
<X size={15} />
|
||||||
</Span>
|
</Span>
|
||||||
{props.children}
|
<Span className={twMerge("text-white")}>{props.children}</Span>
|
||||||
</Card>
|
</Card>
|
||||||
);
|
);
|
||||||
|
|
||||||
@ -81,7 +99,7 @@ export default function Toast({
|
|||||||
const root = createRoot(wrapperEl);
|
const root = createRoot(wrapperEl);
|
||||||
root.render(toastEl);
|
root.render(toastEl);
|
||||||
|
|
||||||
setTimeout(() => {
|
timeout = setTimeout(() => {
|
||||||
closeToast({ wrapperEl });
|
closeToast({ wrapperEl });
|
||||||
setOpen?.(false);
|
setOpen?.(false);
|
||||||
}, closeDelay);
|
}, closeDelay);
|
||||||
|
164
components/lib/form/FileUpload.tsx
Normal file
164
components/lib/form/FileUpload.tsx
Normal file
@ -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>,
|
||||||
|
HTMLDivElement
|
||||||
|
> & {
|
||||||
|
onChangeHandler?: (
|
||||||
|
imgData: FileInputToBase64FunctionReturn | undefined
|
||||||
|
) => any;
|
||||||
|
fileInputProps?: DetailedHTMLProps<
|
||||||
|
React.InputHTMLAttributes<HTMLInputElement>,
|
||||||
|
HTMLInputElement
|
||||||
|
>;
|
||||||
|
placeHolderWrapper?: DetailedHTMLProps<
|
||||||
|
React.HTMLAttributes<HTMLDivElement>,
|
||||||
|
HTMLDivElement
|
||||||
|
>;
|
||||||
|
previewImageWrapperProps?: DetailedHTMLProps<
|
||||||
|
React.HTMLAttributes<HTMLDivElement>,
|
||||||
|
HTMLDivElement
|
||||||
|
>;
|
||||||
|
previewImageProps?: DetailedHTMLProps<
|
||||||
|
React.ImgHTMLAttributes<HTMLImageElement>,
|
||||||
|
HTMLImageElement
|
||||||
|
>;
|
||||||
|
label?: string;
|
||||||
|
disablePreview?: boolean;
|
||||||
|
allowedRegex?: RegExp;
|
||||||
|
externalSetFile?: React.Dispatch<
|
||||||
|
React.SetStateAction<FileInputToBase64FunctionReturn | undefined>
|
||||||
|
>;
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @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<HTMLInputElement>();
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Stack
|
||||||
|
{...props}
|
||||||
|
className={twMerge("w-full h-[300px]", props?.className)}
|
||||||
|
>
|
||||||
|
<input
|
||||||
|
type="file"
|
||||||
|
className={twMerge("hidden", fileInputProps?.className)}
|
||||||
|
{...fileInputProps}
|
||||||
|
onChange={(e) => {
|
||||||
|
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 ? (
|
||||||
|
<Card
|
||||||
|
className="w-full relative h-full items-center justify-center overflow-hidden"
|
||||||
|
{...previewImageWrapperProps}
|
||||||
|
>
|
||||||
|
{disablePreview ? (
|
||||||
|
<Span className="opacity-50" size="small">
|
||||||
|
Image Uploaded!
|
||||||
|
</Span>
|
||||||
|
) : file.fileType?.match(/image/i) ? (
|
||||||
|
<img
|
||||||
|
src={file.fileBase64Full}
|
||||||
|
className="w-full object-contain overflow-hidden"
|
||||||
|
{...previewImageProps}
|
||||||
|
/>
|
||||||
|
) : (
|
||||||
|
<Row>
|
||||||
|
<FileArchive size={36} strokeWidth={1} />
|
||||||
|
<Stack className="gap-0">
|
||||||
|
<Span>{file.file?.name || file.fileName}</Span>
|
||||||
|
<Span size="smaller" className="opacity-70">
|
||||||
|
{file.fileType}
|
||||||
|
</Span>
|
||||||
|
</Stack>
|
||||||
|
</Row>
|
||||||
|
)}
|
||||||
|
<Button
|
||||||
|
variant="ghost"
|
||||||
|
className={twMerge(
|
||||||
|
"absolute p-2 top-2 right-2 z-20 bg-white dark:bg-black",
|
||||||
|
"hover:bg-white dark:hover:bg-black"
|
||||||
|
)}
|
||||||
|
onClick={(e) => {
|
||||||
|
setFile(undefined);
|
||||||
|
externalSetFile?.(undefined);
|
||||||
|
onChangeHandler?.(undefined);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<X className="text-slate-950 dark:text-white" />
|
||||||
|
</Button>
|
||||||
|
</Card>
|
||||||
|
) : (
|
||||||
|
<Card
|
||||||
|
className={twMerge(
|
||||||
|
"w-full h-full cursor-pointer hover:bg-slate-100 dark:hover:bg-white/20",
|
||||||
|
placeHolderWrapper?.className
|
||||||
|
)}
|
||||||
|
onClick={(e) => {
|
||||||
|
inputRef.current?.click();
|
||||||
|
placeHolderWrapper?.onClick?.(e);
|
||||||
|
}}
|
||||||
|
{...placeHolderWrapper}
|
||||||
|
>
|
||||||
|
<Center>
|
||||||
|
<Stack className="items-center gap-2">
|
||||||
|
<FilePlus2 className="text-slate-400" />
|
||||||
|
<Span size="smaller" variant="faded">
|
||||||
|
{label || "Click to Upload File"}
|
||||||
|
</Span>
|
||||||
|
</Stack>
|
||||||
|
</Center>
|
||||||
|
</Card>
|
||||||
|
)}
|
||||||
|
</Stack>
|
||||||
|
);
|
||||||
|
}
|
@ -135,15 +135,7 @@ export default function Input<KeyType extends string>({
|
|||||||
...props
|
...props
|
||||||
}: InputProps<KeyType>) {
|
}: InputProps<KeyType>) {
|
||||||
const [focus, setFocus] = React.useState(false);
|
const [focus, setFocus] = React.useState(false);
|
||||||
const [value, setValue] = React.useState(
|
const [value, setValue] = React.useState(props.defaultValue || props.value);
|
||||||
props.value
|
|
||||||
? String(props.value)
|
|
||||||
: props.defaultValue
|
|
||||||
? String(props.defaultValue)
|
|
||||||
: ""
|
|
||||||
);
|
|
||||||
|
|
||||||
delete props.defaultValue;
|
|
||||||
|
|
||||||
const [isValid, setIsValid] = React.useState(true);
|
const [isValid, setIsValid] = React.useState(true);
|
||||||
|
|
||||||
@ -151,27 +143,28 @@ export default function Input<KeyType extends string>({
|
|||||||
const finalDebounce = debounce || DEFAULT_DEBOUNCE;
|
const finalDebounce = debounce || DEFAULT_DEBOUNCE;
|
||||||
|
|
||||||
React.useEffect(() => {
|
React.useEffect(() => {
|
||||||
if (!value.match(/./)) return setIsValid(true);
|
if (typeof value == "string") {
|
||||||
window.clearTimeout(timeout);
|
if (!value.match(/./)) return setIsValid(true);
|
||||||
|
window.clearTimeout(timeout);
|
||||||
|
|
||||||
if (validationRegex) {
|
if (validationRegex) {
|
||||||
timeout = setTimeout(() => {
|
timeout = setTimeout(() => {
|
||||||
setIsValid(validationRegex.test(value));
|
setIsValid(validationRegex.test(value));
|
||||||
}, finalDebounce);
|
}, finalDebounce);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (validationFunction) {
|
if (validationFunction) {
|
||||||
timeout = setTimeout(() => {
|
timeout = setTimeout(() => {
|
||||||
validationFunction(value).then((res) => {
|
validationFunction(value).then((res) => {
|
||||||
setIsValid(res);
|
setIsValid(res);
|
||||||
});
|
});
|
||||||
}, finalDebounce);
|
}, finalDebounce);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}, [value]);
|
}, [value]);
|
||||||
|
|
||||||
React.useEffect(() => {
|
React.useEffect(() => {
|
||||||
if (!props.value) return;
|
setValue(props.value || "");
|
||||||
setValue(String(props.value));
|
|
||||||
}, [props.value]);
|
}, [props.value]);
|
||||||
|
|
||||||
const targetComponent = istextarea ? (
|
const targetComponent = istextarea ? (
|
||||||
@ -192,7 +185,10 @@ export default function Input<KeyType extends string>({
|
|||||||
props?.onBlur?.(e);
|
props?.onBlur?.(e);
|
||||||
}}
|
}}
|
||||||
value={value}
|
value={value}
|
||||||
onChange={(e) => setValue(e.target.value)}
|
onChange={(e) => {
|
||||||
|
setValue(e.target.value);
|
||||||
|
props?.onChange?.(e);
|
||||||
|
}}
|
||||||
autoComplete={autoComplete}
|
autoComplete={autoComplete}
|
||||||
rows={props.height ? Number(props.height) : 4}
|
rows={props.height ? Number(props.height) : 4}
|
||||||
/>
|
/>
|
||||||
|
@ -89,7 +89,7 @@ export default function Select({
|
|||||||
props.className
|
props.className
|
||||||
)}
|
)}
|
||||||
ref={componentRef}
|
ref={componentRef}
|
||||||
defaultValue={
|
value={
|
||||||
options.flat().find((opt) => opt.default)?.value ||
|
options.flat().find((opt) => opt.default)?.value ||
|
||||||
undefined
|
undefined
|
||||||
}
|
}
|
||||||
|
37
components/lib/hooks/useCustomEventDispatch.tsx
Normal file
37
components/lib/hooks/useCustomEventDispatch.tsx
Normal file
@ -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 };
|
||||||
|
}
|
39
components/lib/hooks/useCustomEventListener.tsx
Normal file
39
components/lib/hooks/useCustomEventListener.tsx
Normal file
@ -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<T | undefined>(undefined);
|
||||||
|
const [message, setMessage] = React.useState<string | undefined>(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 };
|
||||||
|
}
|
31
components/lib/hooks/useLocalStorage.tsx
Normal file
31
components/lib/hooks/useLocalStorage.tsx
Normal file
@ -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<string, any> | undefined = undefined
|
||||||
|
>(param?: UseLocalStorageParam) {
|
||||||
|
const [data, setData] =
|
||||||
|
React.useState<T extends undefined ? string | null : T>();
|
||||||
|
|
||||||
|
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 };
|
||||||
|
}
|
@ -10,10 +10,10 @@ let reconnectInterval: any;
|
|||||||
let msgInterval: any;
|
let msgInterval: any;
|
||||||
let sendInterval: any;
|
let sendInterval: any;
|
||||||
|
|
||||||
let tries = 0;
|
|
||||||
|
|
||||||
export const WebSocketEventNames = ["wsDataEvent", "wsMessageEvent"] as const;
|
export const WebSocketEventNames = ["wsDataEvent", "wsMessageEvent"] as const;
|
||||||
|
|
||||||
|
let tries = 0;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* # Use Websocket Hook
|
* # Use Websocket Hook
|
||||||
* @event wsDataEvent Listen for event named `wsDataEvent` on `window` to receive Data events
|
* @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<string[]>([]);
|
const messageQueueRef = React.useRef<string[]>([]);
|
||||||
const sendMessageQueueRef = React.useRef<string[]>([]);
|
const sendMessageQueueRef = React.useRef<string[]>([]);
|
||||||
|
|
||||||
// const [message, setMessage] = React.useState<string>("");
|
|
||||||
// const [data, setData] = React.useState<T | null>(null);
|
|
||||||
const [refresh, setRefresh] = React.useState(0);
|
|
||||||
|
|
||||||
const dispatchCustomEvent = React.useCallback(
|
const dispatchCustomEvent = React.useCallback(
|
||||||
(evtName: (typeof WebSocketEventNames)[number], value: string | T) => {
|
(evtName: (typeof WebSocketEventNames)[number], value: string | T) => {
|
||||||
const event = new CustomEvent(evtName, {
|
const event = new CustomEvent(evtName, {
|
||||||
@ -55,16 +51,17 @@ export default function useWebSocket<
|
|||||||
[]
|
[]
|
||||||
);
|
);
|
||||||
|
|
||||||
React.useEffect(() => {
|
const connect = React.useCallback(() => {
|
||||||
const wsURL = url;
|
const wsURL = url;
|
||||||
if (!wsURL) return;
|
if (!wsURL) return;
|
||||||
|
|
||||||
const ws = new WebSocket(wsURL);
|
let ws = new WebSocket(wsURL);
|
||||||
|
|
||||||
ws.onopen = (ev) => {
|
ws.onopen = (ev) => {
|
||||||
window.clearInterval(reconnectInterval);
|
window.clearInterval(reconnectInterval);
|
||||||
setSocket(ws);
|
setSocket(ws);
|
||||||
tries = 0;
|
tries = 0;
|
||||||
|
console.log(`Websocket connected to ${wsURL}`);
|
||||||
};
|
};
|
||||||
|
|
||||||
ws.onmessage = (ev) => {
|
ws.onmessage = (ev) => {
|
||||||
@ -83,16 +80,22 @@ export default function useWebSocket<
|
|||||||
if (tries >= 3) {
|
if (tries >= 3) {
|
||||||
return window.clearInterval(reconnectInterval);
|
return window.clearInterval(reconnectInterval);
|
||||||
}
|
}
|
||||||
|
|
||||||
console.log("Attempting to reconnect ...");
|
console.log("Attempting to reconnect ...");
|
||||||
setRefresh(refresh + 1);
|
|
||||||
tries++;
|
tries++;
|
||||||
|
|
||||||
|
connect();
|
||||||
}, 1000);
|
}, 1000);
|
||||||
};
|
};
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
React.useEffect(() => {
|
||||||
|
connect();
|
||||||
|
|
||||||
return function () {
|
return function () {
|
||||||
window.clearInterval(reconnectInterval);
|
window.clearInterval(reconnectInterval);
|
||||||
};
|
};
|
||||||
}, [refresh]);
|
}, []);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Received Message Queue Handler
|
* Received Message Queue Handler
|
||||||
@ -103,8 +106,6 @@ export default function useWebSocket<
|
|||||||
if (!newMessage) return;
|
if (!newMessage) return;
|
||||||
try {
|
try {
|
||||||
const jsonData = JSON.parse(newMessage);
|
const jsonData = JSON.parse(newMessage);
|
||||||
// setData(jsonData);
|
|
||||||
|
|
||||||
dispatchCustomEvent("wsMessageEvent", newMessage);
|
dispatchCustomEvent("wsMessageEvent", newMessage);
|
||||||
dispatchCustomEvent("wsDataEvent", jsonData);
|
dispatchCustomEvent("wsDataEvent", jsonData);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
|
@ -119,26 +119,26 @@ export default function Button({
|
|||||||
} else if (variant == "ghost") {
|
} else if (variant == "ghost") {
|
||||||
if (color == "primary" || !color)
|
if (color == "primary" || !color)
|
||||||
return twMerge(
|
return twMerge(
|
||||||
"bg-transparent outline-none p-2",
|
"bg-transparent dark:bg-transparent outline-none p-2",
|
||||||
"text-blue-500",
|
"text-blue-500 hover:bg-transparent dark:hover:bg-transparent",
|
||||||
"twui-button-primary-ghost"
|
"twui-button-primary-ghost"
|
||||||
);
|
);
|
||||||
if (color == "secondary")
|
if (color == "secondary")
|
||||||
return twMerge(
|
return twMerge(
|
||||||
"bg-transparent outline-none p-2",
|
"bg-transparent dark:bg-transparent outline-none p-2",
|
||||||
"text-emerald-500",
|
"text-emerald-500 hover:bg-transparent dark:hover:bg-transparent",
|
||||||
"twui-button-secondary-ghost"
|
"twui-button-secondary-ghost"
|
||||||
);
|
);
|
||||||
if (color == "accent")
|
if (color == "accent")
|
||||||
return twMerge(
|
return twMerge(
|
||||||
"bg-transparent outline-none p-2",
|
"bg-transparent dark:bg-transparent outline-none p-2",
|
||||||
"text-violet-500",
|
"text-violet-500 hover:bg-transparent dark:hover:bg-transparent",
|
||||||
"twui-button-accent-ghost"
|
"twui-button-accent-ghost"
|
||||||
);
|
);
|
||||||
if (color == "gray")
|
if (color == "gray")
|
||||||
return twMerge(
|
return twMerge(
|
||||||
"bg-transparent outline-none p-2",
|
"bg-transparent dark:bg-transparent outline-none p-2 hover:bg-transparent dark:hover:bg-transparent",
|
||||||
"text-slate-600 dark:text-white/70",
|
"text-slate-600 dark:text-white/70 hover:opacity-80",
|
||||||
"twui-button-gray-ghost"
|
"twui-button-gray-ghost"
|
||||||
);
|
);
|
||||||
if (color == "error")
|
if (color == "error")
|
||||||
|
@ -17,7 +17,7 @@ export default function Divider({
|
|||||||
<div
|
<div
|
||||||
{...props}
|
{...props}
|
||||||
className={twMerge(
|
className={twMerge(
|
||||||
"border-slate-200 dark:border-white/10",
|
"border-slate-200 dark:border-white/10 border-solid",
|
||||||
vertical
|
vertical
|
||||||
? "border-0 border-l h-full min-h-5"
|
? "border-0 border-l h-full min-h-5"
|
||||||
: "border-0 border-t w-full",
|
: "border-0 border-t w-full",
|
||||||
|
@ -1,16 +1,26 @@
|
|||||||
import { AnchorHTMLAttributes, DetailedHTMLProps } from "react";
|
import { AnchorHTMLAttributes, DetailedHTMLProps, RefAttributes } from "react";
|
||||||
import { twMerge } from "tailwind-merge";
|
import { twMerge } from "tailwind-merge";
|
||||||
|
import { ArrowUpRight, LucideProps } from "lucide-react";
|
||||||
|
|
||||||
|
type Props = DetailedHTMLProps<
|
||||||
|
AnchorHTMLAttributes<HTMLAnchorElement>,
|
||||||
|
HTMLAnchorElement
|
||||||
|
> & {
|
||||||
|
showArrow?: boolean;
|
||||||
|
arrowSize?: number;
|
||||||
|
arrowProps?: Omit<LucideProps, "ref"> & RefAttributes<SVGSVGElement>;
|
||||||
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* # General Anchor Elements
|
* # General Anchor Elements
|
||||||
* @className twui-a | twui-anchor
|
* @className twui-a | twui-anchor
|
||||||
*/
|
*/
|
||||||
export default function Link({
|
export default function Link({
|
||||||
|
showArrow,
|
||||||
|
arrowSize = 20,
|
||||||
|
arrowProps,
|
||||||
...props
|
...props
|
||||||
}: DetailedHTMLProps<
|
}: Props) {
|
||||||
AnchorHTMLAttributes<HTMLAnchorElement>,
|
|
||||||
HTMLAnchorElement
|
|
||||||
>) {
|
|
||||||
return (
|
return (
|
||||||
<a
|
<a
|
||||||
{...props}
|
{...props}
|
||||||
@ -18,13 +28,22 @@ export default function Link({
|
|||||||
"text-base text-link-500 no-underline hover:text-link-500/50",
|
"text-base text-link-500 no-underline hover:text-link-500/50",
|
||||||
"text-blue-600 dark:text-blue-400 hover:opacity-60 transition-all",
|
"text-blue-600 dark:text-blue-400 hover:opacity-60 transition-all",
|
||||||
"border-0 border-b border-blue-300 dark:border-blue-200/30 border-solid leading-4",
|
"border-0 border-b border-blue-300 dark:border-blue-200/30 border-solid leading-4",
|
||||||
// "focus:text-red-600",
|
|
||||||
"twui-anchor",
|
"twui-anchor",
|
||||||
"twui-a",
|
"twui-a",
|
||||||
props.className
|
props.className
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
{props.children}
|
{props.children}
|
||||||
|
{showArrow && (
|
||||||
|
<ArrowUpRight
|
||||||
|
size={arrowSize}
|
||||||
|
{...arrowProps}
|
||||||
|
className={twMerge(
|
||||||
|
"inline-block ml-1 -mt-[1px]",
|
||||||
|
arrowProps?.className
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
</a>
|
</a>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -2,15 +2,18 @@ import _ from "lodash";
|
|||||||
import { DetailedHTMLProps, HTMLAttributes } from "react";
|
import { DetailedHTMLProps, HTMLAttributes } from "react";
|
||||||
import { twMerge } from "tailwind-merge";
|
import { twMerge } from "tailwind-merge";
|
||||||
|
|
||||||
|
type Props = DetailedHTMLProps<
|
||||||
|
HTMLAttributes<HTMLDivElement>,
|
||||||
|
HTMLDivElement
|
||||||
|
> & {
|
||||||
|
center?: boolean;
|
||||||
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* # Flexbox Column
|
* # Flexbox Column
|
||||||
* @className twui-stack
|
* @className twui-stack
|
||||||
*/
|
*/
|
||||||
export default function Stack({
|
export default function Stack({ ...props }: Props) {
|
||||||
...props
|
|
||||||
}: DetailedHTMLProps<HTMLAttributes<HTMLDivElement>, HTMLDivElement> & {
|
|
||||||
center?: boolean;
|
|
||||||
}) {
|
|
||||||
const finalProps = _.omit(props, "center");
|
const finalProps = _.omit(props, "center");
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
|
39
components/lib/next-js/hooks/useMDXComponents.tsx
Normal file
39
components/lib/next-js/hooks/useMDXComponents.tsx
Normal file
@ -0,0 +1,39 @@
|
|||||||
|
import React from "react";
|
||||||
|
import type { MDXComponents } from "mdx/types";
|
||||||
|
import H1 from "../../layout/H1";
|
||||||
|
import H2 from "../../layout/H2";
|
||||||
|
import H3 from "../../layout/H3";
|
||||||
|
import H4 from "../../layout/H4";
|
||||||
|
import CodeBlock from "../../elements/CodeBlock";
|
||||||
|
|
||||||
|
type Params = {
|
||||||
|
components: MDXComponents;
|
||||||
|
codeBgColor?: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
export default function useMDXComponents({
|
||||||
|
components,
|
||||||
|
codeBgColor,
|
||||||
|
}: Params): MDXComponents {
|
||||||
|
return {
|
||||||
|
h1: ({ children }) => <H1>{children}</H1>,
|
||||||
|
h2: ({ children }) => <H2>{children}</H2>,
|
||||||
|
h3: ({ children }) => <H3>{children}</H3>,
|
||||||
|
h4: ({ children }) => <H4>{children}</H4>,
|
||||||
|
pre: ({ children, ...props }) => {
|
||||||
|
if (React.isValidElement(children) && children.props) {
|
||||||
|
return (
|
||||||
|
<CodeBlock {...props} backgroundColor={codeBgColor}>
|
||||||
|
{children.props.children}
|
||||||
|
</CodeBlock>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return (
|
||||||
|
<CodeBlock {...props} backgroundColor={codeBgColor}>
|
||||||
|
{children}
|
||||||
|
</CodeBlock>
|
||||||
|
);
|
||||||
|
},
|
||||||
|
...components,
|
||||||
|
};
|
||||||
|
}
|
27
components/lib/package.json
Normal file
27
components/lib/package.json
Normal file
@ -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"
|
||||||
|
}
|
||||||
|
}
|
59
components/lib/utils/form/fileInputToBase64.ts
Normal file
59
components/lib/utils/form/fileInputToBase64.ts
Normal file
@ -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<FileInputToBase64FunctionReturn> {
|
||||||
|
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,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
@ -59,7 +59,7 @@ export const SocialLinks: SocialLinksType[] = [
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "Git",
|
name: "Git",
|
||||||
href: "https://git.tben.me",
|
href: "https://git.tben.me/explore/repos",
|
||||||
icon: <GitBranch size={17} />,
|
icon: <GitBranch size={17} />,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
Loading…
Reference in New Issue
Block a user