This commit is contained in:
Benjamin Toby
2025-07-20 10:35:54 +01:00
parent dd1d05251d
commit a0a0ab8ee4
99 changed files with 5678 additions and 909 deletions
+25
View File
@@ -0,0 +1,25 @@
import { ComponentProps, ReactNode } from "react";
import { ArrowUpRight } from "lucide-react";
import { TWUI_LINK_LIST_LINK_OBJECT } from "../elements/LinkList";
import Link from "./Link";
import Row from "./Row";
type Props = ComponentProps<typeof Link> & {
link: TWUI_LINK_LIST_LINK_OBJECT;
icon?: ReactNode;
};
/**
* # Link With an Arrow
* @className twui-arrowed-link
*/
export default function ArrowedLink({ link, icon, ...props }: Props) {
return (
<Link href={link.url} {...props} {...link.linkProps}>
<Row>
<span>{link.title}</span>
{icon || <ArrowUpRight size={17} />}
</Row>
</Link>
);
}
+123 -24
View File
@@ -1,6 +1,7 @@
import {
AnchorHTMLAttributes,
ButtonHTMLAttributes,
ComponentProps,
DetailedHTMLProps,
HTMLAttributeAnchorTarget,
HTMLAttributes,
@@ -12,10 +13,13 @@ export type TWUIButtonProps = DetailedHTMLProps<
ButtonHTMLAttributes<HTMLButtonElement>,
HTMLButtonElement
> & {
title: string;
variant?: "normal" | "ghost" | "outlined";
color?:
| "primary"
| "secondary"
| "text"
| "white"
| "accent"
| "gray"
| "error"
@@ -36,6 +40,7 @@ export type TWUIButtonProps = DetailedHTMLProps<
HTMLAttributes<HTMLDivElement>,
HTMLDivElement
>;
loadingProps?: ComponentProps<typeof Loading>;
};
/**
@@ -48,12 +53,48 @@ export type TWUIButtonProps = DetailedHTMLProps<
* @className twui-button-secondary
* @className twui-button-secondary-outlined
* @className twui-button-secondary-ghost
* @className twui-button-white
* @className twui-button-white-outlined
* @className twui-button-white-ghost
* @className twui-button-accent
* @className twui-button-accent-outlined
* @className twui-button-accent-ghost
* @className twui-button-gray
* @className twui-button-gray-outlined
* @className twui-button-gray-ghost
*
* @example
```css
CSS directive:
//@theme inline {
--breakpoint-xs: 350px;
--color-primary: #000000;
--color-primary-hover: #29292b;
--color-primary-outline: #29292b;
--color-primary-text: #29292b;
--color-primary-dark: #29292b;
--color-primary-dark-hover: #4b4b4b;
--color-primary-dark-outline: #4b4b4b;
--color-primary-dark-text: #4b4b4b;
--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-accent: #000000;
--color-accent-hover: #dddddd;
--color-accent-outline: #dddddd;
--color-accent-text: #dddddd;
--color-accent-dark: #000000;
--color-accent-dark-hover: #dddddd;
--color-accent-dark-outline: #dddddd;
--color-accent-dark-text: #dddddd;
}
```
*/
export default function Button({
href,
@@ -67,47 +108,67 @@ export default function Button({
afterIcon,
loading,
loadingIconSize,
loadingProps,
...props
}: TWUIButtonProps) {
const finalClassName: string = (() => {
if (variant == "normal" || !variant) {
if (color == "primary" || !color)
return twMerge(
"bg-blue-500 hover:bg-blue-600 text-white",
"bg-primary hover:bg-primary-hover text-white",
"dark:bg-primary-dark hover:dark:bg-primary-dark-hover text-white",
"twui-button-primary"
);
if (color == "secondary")
return twMerge(
"bg-emerald-500 hover:bg-emerald-600 text-white",
"bg-secondary hover:bg-secondary-hover text-white",
"twui-button-secondary"
);
if (color == "white")
return twMerge(
"!bg-white hover:!bg-slate-200 !text-slate-800",
"twui-button-white"
);
if (color == "accent")
return twMerge(
"bg-violet-500 hover:bg-violet-600 text-white",
"bg-accent hover:bg-accent-hover text-white",
"twui-button-accent"
);
if (color == "gray")
return twMerge(
"bg-slate-300 hover:bg-slate-200 text-slate-800",
"bg-gray hover:bg-gray-hover text-foreground-light",
"dark:bg-gray-dark hover:dark:bg-gray-dark-hover dark:text-foreground-dark",
"twui-button-gray"
);
if (color == "success")
return twMerge(
"bg-success hover:bg-success-hover text-white",
"dark:bg-success hover:dark:bg-success-hover text-white",
"twui-button-success"
);
if (color == "error")
return twMerge(
"bg-error hover:bg-error-hover text-white",
"dark:bg-error hover:dark:bg-error-hover text-white",
"twui-button-error"
);
} else if (variant == "outlined") {
if (color == "primary" || !color)
return twMerge(
"bg-transparent outline outline-1 outline-blue-500",
"text-blue-500 dark:text-blue-400 dark:outline-blue-300",
"bg-transparent outline outline-1 outline-primary",
"text-primary-text dark:text-primary-dark-text dark:outline-primary-dark-outline",
"twui-button-primary-outlined"
);
if (color == "secondary")
return twMerge(
"bg-transparent outline outline-1 outline-emerald-500",
"text-emerald-500",
"bg-transparent outline outline-1 outline-secondary",
"text-secondary",
"twui-button-secondary-outlined"
);
if (color == "accent")
return twMerge(
"bg-transparent outline outline-1 outline-violet-500",
"text-violet-500",
"bg-transparent outline outline-1 outline-accent",
"text-accent",
"twui-button-accent-outlined"
);
if (color == "gray")
@@ -116,23 +177,41 @@ export default function Button({
"text-slate-600 dark:text-white/60 dark:outline-white/30",
"twui-button-gray-outlined"
);
if (color == "white")
return twMerge(
"bg-transparent outline outline-1 outline-white/50",
"text-white",
"twui-button-white-outlined"
);
if (color == "error")
return twMerge(
"bg-transparent outline outline-1 outline-error text-error",
"dark:outline-error dark:text-error-dark",
"twui-button-error-outlined"
);
} else if (variant == "ghost") {
if (color == "primary" || !color)
return twMerge(
"bg-transparent dark:bg-transparent outline-none p-2",
"text-blue-500 hover:bg-transparent dark:hover:bg-transparent",
"text-primary-text dark:text-primary-dark-text hover:bg-transparent dark:hover:bg-transparent",
"twui-button-primary-ghost"
);
if (color == "secondary")
return twMerge(
"bg-transparent dark:bg-transparent outline-none p-2",
"text-emerald-500 hover:bg-transparent dark:hover:bg-transparent",
"text-secondary hover:bg-transparent dark:hover:bg-transparent",
"twui-button-secondary-ghost"
);
if (color == "text")
return twMerge(
"bg-transparent dark:bg-transparent outline-none p-2 dark:text-foreground-dark",
"text-foreground-light hover:bg-transparent dark:hover:bg-transparent",
"twui-button-secondary-ghost"
);
if (color == "accent")
return twMerge(
"bg-transparent dark:bg-transparent outline-none p-2",
"text-violet-500 hover:bg-transparent dark:hover:bg-transparent",
"text-accent hover:bg-transparent dark:hover:bg-transparent",
"twui-button-accent-ghost"
);
if (color == "gray")
@@ -156,9 +235,15 @@ export default function Button({
if (color == "success")
return twMerge(
"bg-transparent outline-none p-2",
"text-emerald-600",
"text-success",
"twui-button-success-ghost"
);
if (color == "white")
return twMerge(
"bg-transparent outline-none p-2",
"text-white",
"twui-button-white-ghost"
);
}
return "";
@@ -168,17 +253,24 @@ export default function Button({
<button
{...props}
className={twMerge(
"bg-blue-600 text-white text-base font-medium px-4 py-2 rounded",
"flex items-center justify-center relative transition-all",
"bg-primary text-white font-medium px-4 py-2 rounded-default",
"flex items-center justify-center relative transition-all cursor-pointer",
props.disabled ? "opacity-40 cursor-not-allowed" : "",
"twui-button-general",
size == "small" && "px-3 py-1.5 text-sm",
size == "smaller" && "px-2 py-1 text-xs",
size == "large" && "text-lg",
size == "larger" && "px-5 py-3 text-xl",
size == "small"
? "px-3 py-1.5 text-sm twui-button-small"
: size == "smaller"
? "px-2 py-1 text-xs twui-button-smaller"
: size == "large"
? "text-lg twui-button-large"
: size == "larger"
? "px-5 py-3 text-xl twui-button-larger"
: "twui-button-base",
finalClassName,
props.className,
loading ? "pointer-events-none opacity-80" : "l"
loading ? "pointer-events-none opacity-80" : "",
props.className
)}
aria-label={props.title}
>
<div
{...buttonContentProps}
@@ -196,7 +288,6 @@ export default function Button({
{loading && (
<Loading
className="absolute"
size={(() => {
if (loadingIconSize) return loadingIconSize;
switch (size) {
@@ -209,6 +300,8 @@ export default function Button({
return "normal";
}
})()}
{...loadingProps}
className={twMerge("absolute", loadingProps?.className)}
/>
)}
</button>
@@ -216,7 +309,13 @@ export default function Button({
if (href)
return (
<a {...linkProps} href={href} target={target}>
<a
{...linkProps}
href={href}
target={target}
title={props.title}
aria-label={props.title}
>
{buttonComponent}
</a>
);
+1 -1
View File
@@ -13,7 +13,7 @@ export default function Center({
{...props}
className={twMerge(
"flex flex-col items-center justify-center gap-4 p-2 w-full",
"twui-center",
"h-full twui-center",
props.className
)}
>
+11 -7
View File
@@ -1,28 +1,32 @@
import { DetailedHTMLProps, HTMLAttributes } from "react";
import { twMerge } from "tailwind-merge";
type Props = DetailedHTMLProps<
HTMLAttributes<HTMLDivElement>,
HTMLDivElement
> & {
vertical?: boolean;
dashed?: boolean;
};
/**
* # Vertical and Horizontal Divider
* @className twui-divider
* @className twui-divider-horizontal
* @className twui-divider-vertical
*/
export default function Divider({
vertical,
...props
}: DetailedHTMLProps<HTMLAttributes<HTMLDivElement>, HTMLDivElement> & {
vertical?: boolean;
}) {
export default function Divider({ vertical, dashed, ...props }: Props) {
return (
<div
{...props}
className={twMerge(
"border-slate-200 dark:border-white/10 border-solid",
"border-slate-200 dark:border-white/10",
vertical
? "border-0 border-l h-full min-h-5"
: "border-0 border-t w-full",
"twui-divider",
vertical ? "twui-divider-vertical" : "twui-divider-horizontal",
dashed ? "border-dashed" : "border-solid",
props.className
)}
/>
+17
View File
@@ -0,0 +1,17 @@
import { twMerge } from "tailwind-merge";
import Img, { TWUIImageProps } from "./Img";
import Border from "../elements/Border";
export default function DocsImg({ ...props }: TWUIImageProps) {
return (
<Border className={twMerge("p-0 mb-10")}>
<Img
{...props}
className={twMerge(
"w-full h-auto rounded-default overflow-hidden",
props.className
)}
/>
</Border>
);
}
+6 -1
View File
@@ -11,7 +11,12 @@ export default function H1({
return (
<h1
{...props}
className={twMerge("text-5xl mb-4", "twui-h1", props.className)}
className={twMerge(
"text-4xl md:text-5xl mb-4",
"twui-headings twui-heading",
"twui-h1",
props.className
)}
>
{props.children}
</h1>
+6 -1
View File
@@ -11,7 +11,12 @@ export default function H2({
return (
<h2
{...props}
className={twMerge("text-3xl mb-4", "twui-h2", props.className)}
className={twMerge(
"text-2xl md:text-3xl mb-4",
"twui-headings twui-heading",
"twui-h2",
props.className
)}
>
{props.children}
</h2>
+6 -1
View File
@@ -11,7 +11,12 @@ export default function H3({
return (
<h3
{...props}
className={twMerge("text-xl mb-4", "twui-h3", props.className)}
className={twMerge(
"text-xl mb-4",
"twui-headings twui-heading",
"twui-h3",
props.className
)}
>
{props.children}
</h3>
+6 -1
View File
@@ -11,7 +11,12 @@ export default function H4({
return (
<h4
{...props}
className={twMerge("text-base mb-4", "twui-h4", props.className)}
className={twMerge(
"text-base mb-4",
"twui-headings twui-heading",
"twui-h4",
props.className
)}
>
{props.children}
</h4>
+6 -1
View File
@@ -11,7 +11,12 @@ export default function H5({
return (
<h5
{...props}
className={twMerge("text-sm mb-4", "twui-h5", props.className)}
className={twMerge(
"text-sm mb-4",
"twui-headings twui-heading",
"twui-h5",
props.className
)}
>
{props.children}
</h5>
+38
View File
@@ -0,0 +1,38 @@
import { ComponentProps, ReactNode } from "react";
import { TWUI_LINK_LIST_LINK_OBJECT } from "../elements/LinkList";
import Link from "./Link";
import Row from "./Row";
type Props = ComponentProps<typeof Link> & {
link: TWUI_LINK_LIST_LINK_OBJECT;
icon: ReactNode;
iconPosition?: "before" | "after";
};
/**
* # Link With an Icon
* @className twui-arrowed-link
*/
export default function IconLink({
link,
iconPosition,
icon,
...props
}: Props) {
return (
<Link
href={link.url}
{...props}
onClick={(e) => {
link.onClick?.(e);
props.onClick?.(e);
}}
>
<Row>
{iconPosition == "before" || !iconPosition ? icon : null}
<span>{link.title}</span>
{iconPosition == "after" ? icon : null}
</Row>
</Link>
);
}
+44 -1
View File
@@ -6,6 +6,7 @@ export type TWUIImageProps = DetailedHTMLProps<
ImgHTMLAttributes<HTMLImageElement>,
HTMLImageElement
> & {
alt: string;
size?: number;
circle?: boolean;
bgImg?: boolean;
@@ -24,6 +25,8 @@ export default function Img({ ...props }: TWUIImageProps) {
const height = props.size || props.height;
const sizeRatio = width && height ? Number(width) / Number(height) : 1;
const [imageError, setImageError] = React.useState(false);
const finalProps = _.omit(props, [
"size",
"circle",
@@ -53,30 +56,70 @@ export default function Img({ ...props }: TWUIImageProps) {
}
props.onError?.(e);
},
style: {
...(props.size
? {
width: `${props.size}px`,
minWidth: `${props.size}px`,
height: `${props.size}px`,
}
: {}),
...props.style,
},
};
if (imageError) {
return (
<img
loading="lazy"
{...interpolatedProps}
src={
"https://static.datasquirel.com/images/user-images/user-2/castcord-image-preset_thumbnail.jpg"
}
/>
);
}
if (props.srcDark && props.srcLight) {
return (
<React.Fragment>
<img
loading="lazy"
{...interpolatedProps}
className={twMerge(
"hidden dark:block",
interpolatedProps.className
)}
src={props.srcDark}
onError={(e) => {
setImageError(true);
props.onError?.(e);
}}
/>
<img
loading="lazy"
{...interpolatedProps}
className={twMerge(
"block dark:hidden",
interpolatedProps.className
)}
src={props.srcLight}
onError={(e) => {
setImageError(true);
props.onError?.(e);
}}
/>
</React.Fragment>
);
}
return <img {...interpolatedProps} />;
return (
<img
{...interpolatedProps}
onError={(e) => {
setImageError(true);
props.onError?.(e);
}}
/>
);
}
+15 -3
View File
@@ -9,29 +9,41 @@ type Props = DetailedHTMLProps<
showArrow?: boolean;
arrowSize?: number;
arrowProps?: Omit<LucideProps, "ref"> & RefAttributes<SVGSVGElement>;
strict?: boolean;
};
/**
* # General Anchor Elements
* @className twui-a | twui-anchor
* @info use `cancel-link` class name to prevent triggering this link from a child element
*/
export default function Link({
showArrow,
arrowSize = 20,
arrowProps,
strict,
...props
}: Props) {
return (
<a
{...props}
className={twMerge(
"text-base text-link-500 no-underline hover:text-link-500/50",
"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",
"text-link-500 no-underline hover:text-link-500/50",
"text-link dark:text-link-dark hover:opacity-80 transition-all",
"border-0 border-b border-link dark:border-link-dark border-solid leading-4",
"twui-anchor",
"twui-a",
props.className
)}
onClick={(e) => {
const target = e.target as HTMLElement;
if (target.closest(".cancel-link")) {
e.preventDefault();
}
props?.onClick?.(e);
}}
data-strict={strict ? "yes" : undefined}
>
{props.children}
{showArrow && (
+10 -4
View File
@@ -1,18 +1,24 @@
import { DetailedHTMLProps, HTMLAttributes } from "react";
import { twMerge } from "tailwind-merge";
type Props = DetailedHTMLProps<
HTMLAttributes<HTMLHeadingElement>,
HTMLHeadingElement
> & {
noMargin?: boolean;
};
/**
* # Paragraph Tag
* @className twui-p | twui-paragraph
*/
export default function P({
...props
}: DetailedHTMLProps<HTMLAttributes<HTMLHeadingElement>, HTMLHeadingElement>) {
export default function P({ noMargin, ...props }: Props) {
return (
<p
{...props}
className={twMerge(
"text-base py-4",
"py-4",
noMargin ? "!m-0 p-0" : "",
"twui-p",
"twui-paragraph",
props.className
+12 -4
View File
@@ -1,18 +1,26 @@
import { DetailedHTMLProps, HTMLAttributes } from "react";
import { twMerge } from "tailwind-merge";
type Props = DetailedHTMLProps<
HTMLAttributes<HTMLDivElement>,
HTMLDivElement
> & {
noWrap?: boolean;
itemsStart?: boolean;
};
/**
* # Flexbox Row
* @className twui-row
*/
export default function Row({
...props
}: DetailedHTMLProps<HTMLAttributes<HTMLDivElement>, HTMLDivElement>) {
export default function Row({ noWrap, itemsStart, ...props }: Props) {
return (
<div
{...props}
className={twMerge(
"flex flex-row items-center gap-2 flex-wrap",
"flex flex-row gap-2",
noWrap ? "xl:flex-nowrap" : "flex-wrap",
itemsStart ? "items-start" : "items-center",
"twui-row",
props.className
)}
+30
View File
@@ -0,0 +1,30 @@
import _ from "lodash";
import { DetailedHTMLProps, HTMLAttributes } from "react";
import { twMerge } from "tailwind-merge";
type Props = DetailedHTMLProps<
HTMLAttributes<HTMLDivElement>,
HTMLDivElement
> & {
horizontal?: boolean;
};
/**
* # Space Component
* @className twui-spacer
*/
export default function Spacer({ horizontal, ...props }: Props) {
return (
<div
{...props}
className={twMerge(
"grow",
horizontal ? "w-10" : "w-full h-10",
"twui-spacer",
props.className
)}
>
{props.children}
</div>
);
}
+1 -1
View File
@@ -17,7 +17,7 @@ export default function Span({
<span
{...props}
className={twMerge(
"text-base",
"",
size == "small" && "text-sm",
size == "smaller" && "text-xs",
size == "large" && "text-lg",
+37
View File
@@ -0,0 +1,37 @@
import _ from "lodash";
import { DetailedHTMLProps, HTMLAttributes } from "react";
import { twMerge } from "tailwind-merge";
type Props = DetailedHTMLProps<
HTMLAttributes<HTMLDivElement>,
HTMLDivElement
> & {
center?: boolean;
gap?: number | string;
};
/**
* # Flexbox Column
* @className twui-stack
*/
export default function Stack({ gap, ...props }: Props) {
const finalProps = _.omit(props, "center");
return (
<div
{...finalProps}
className={twMerge(
"flex flex-col items-start gap-4",
props.center && "items-center",
gap
? typeof gap == "string"
? `gap-[${gap}]`
: `gap-${gap}`
: "",
"twui-stack",
props.className
)}
>
{props.children}
</div>
);
}
+9 -1
View File
@@ -7,13 +7,15 @@ type Props = DetailedHTMLProps<
HTMLDivElement
> & {
center?: boolean;
gap?: number | string;
componentRef?: React.RefObject<HTMLDivElement>;
};
/**
* # Flexbox Column
* @className twui-stack
*/
export default function Stack({ ...props }: Props) {
export default function Stack({ gap, componentRef, ...props }: Props) {
const finalProps = _.omit(props, "center");
return (
<div
@@ -21,9 +23,15 @@ export default function Stack({ ...props }: Props) {
className={twMerge(
"flex flex-col items-start gap-4",
props.center && "items-center",
gap
? typeof gap == "string"
? `gap-[${gap}]`
: `gap-${gap}`
: "",
"twui-stack",
props.className
)}
ref={componentRef}
>
{props.children}
</div>