This commit is contained in:
Benjamin Toby
2025-03-27 07:37:16 +01:00
parent 9dd6c3a70e
commit 8762e2da8d
23 changed files with 968 additions and 88 deletions
+56 -23
View File
@@ -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<LinkObject[] | null>(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 (
<Row className="gap-4 flex-nowrap whitespace-nowrap overflow-x-auto w-full">
{links.map((linkObject, index, array) => {
if (index === links.length - 1) {
return (
<Link
key={index}
href={linkObject.path}
className="text-slate-400 dark:text-slate-500 pointer-events-none text-xs"
>
{linkObject.title}
</Link>
);
} else {
return (
<React.Fragment key={index}>
<Link href={linkObject.path} className="text-xs">
<div
className={twMerge(
"overflow-x-auto max-w-[70vw]",
"twui-current-breadcrumb-wrapper"
)}
>
<Row className="gap-4 flex-nowrap whitespace-nowrap overflow-x-auto w-full">
{links.map((linkObject, index, array) => {
const isTarget = array.length - 1 == index;
if (index === links.length - 1) {
return (
<Link
key={index}
href={linkObject.path}
className={twMerge(
"text-slate-400 dark:text-slate-500 pointer-events-none text-xs",
isTarget ? "current" : "",
"twui-current-breadcrumb-link"
)}
>
{linkObject.title}
</Link>
<Divider vertical />
</React.Fragment>
);
}
})}
</Row>
);
} else {
return (
<React.Fragment key={index}>
<Link
href={linkObject.path}
className={twMerge(
"text-xs",
isTarget ? "current" : "",
"twui-current-breadcrumb-link"
)}
>
{linkObject.title}
</Link>
<Divider vertical />
</React.Fragment>
);
}
})}
</Row>
</div>
);
////////////////////////////////////////
////////////////////////////////////////
+151
View 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>
);
}
+5 -1
View File
@@ -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}
>
<div
onClick={() => {
onClick={(e) => {
const targetEl = e.target as HTMLElement | null;
if (targetEl?.closest?.(".cancel-link")) return;
externalSetOpen?.(!open);
setOpen(!open);
}}
@@ -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>
);
}
+23 -5
View File
@@ -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>,
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);
}}
>
<Span
className={twMerge(
"absolute top-2 right-2 z-[100] cursor-pointer"
"absolute top-2 right-2 z-[100] cursor-pointer",
"text-white"
)}
onClick={(e) => {
const targetEl = e.target as HTMLElement;
@@ -64,7 +82,7 @@ export default function Toast({
>
<X size={15} />
</Span>
{props.children}
<Span className={twMerge("text-white")}>{props.children}</Span>
</Card>
);
@@ -81,7 +99,7 @@ export default function Toast({
const root = createRoot(wrapperEl);
root.render(toastEl);
setTimeout(() => {
timeout = setTimeout(() => {
closeToast({ wrapperEl });
setOpen?.(false);
}, closeDelay);