This commit is contained in:
2026-03-04 17:35:14 +01:00
parent f73b56cdc4
commit db26e26495
113 changed files with 12433 additions and 169 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>
);
}
+230 -52
View File
@@ -1,11 +1,48 @@
import {
import React, {
AnchorHTMLAttributes,
ButtonHTMLAttributes,
ComponentProps,
DetailedHTMLProps,
HTMLAttributeAnchorTarget,
HTMLAttributes,
} from "react";
import { twMerge } from "tailwind-merge";
import Loading from "../elements/Loading";
import LucideIcon, { TWUILucideIconName } from "../elements/lucide-icon";
export type TWUIButtonProps = DetailedHTMLProps<
ButtonHTMLAttributes<HTMLButtonElement>,
HTMLButtonElement
> & {
title: string;
variant?: "normal" | "ghost" | "outlined";
color?:
| "primary"
| "secondary"
| "text"
| "white"
| "accent"
| "gray"
| "error"
| "warning"
| "success";
size?: "small" | "smaller" | "normal" | "large" | "larger";
loadingIconSize?: React.ComponentProps<typeof Loading>["size"];
href?: string;
target?: HTMLAttributeAnchorTarget;
loading?: boolean;
linkProps?: DetailedHTMLProps<
AnchorHTMLAttributes<HTMLAnchorElement>,
HTMLAnchorElement
>;
beforeIcon?: TWUILucideIconName | React.JSX.Element;
afterIcon?: TWUILucideIconName | React.JSX.Element;
buttonContentProps?: DetailedHTMLProps<
HTMLAttributes<HTMLDivElement>,
HTMLDivElement
>;
loadingProps?: ComponentProps<typeof Loading>;
};
/**
* # Buttons
@@ -17,106 +54,196 @@ import Loading from "../elements/Loading";
* @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,
target,
variant,
color,
size,
buttonContentProps,
linkProps,
beforeIcon,
afterIcon,
loading,
loadingIconSize,
loadingProps,
...props
}: DetailedHTMLProps<
ButtonHTMLAttributes<HTMLButtonElement>,
HTMLButtonElement
> & {
variant?: "normal" | "ghost" | "outlined";
color?: "primary" | "secondary" | "accent" | "gray";
href?: string;
target?: HTMLAttributeAnchorTarget;
loading?: boolean;
linkProps?: DetailedHTMLProps<
AnchorHTMLAttributes<HTMLAnchorElement>,
HTMLAnchorElement
>;
}) {
}: TWUIButtonProps) {
const finalClassName: string = (() => {
if (variant == "normal" || !variant) {
if (color == "primary" || !color)
return twMerge(
"bg-blue-500 hover:bg-blue-600 text-white",
"twui-button-primary"
"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",
"twui-button-secondary"
"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",
"twui-button-accent"
"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",
"twui-button-gray"
"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",
"twui-button-primary-outlined"
"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",
"twui-button-secondary-outlined"
"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",
"twui-button-accent-outlined"
"bg-transparent outline outline-1 outline-accent",
"text-accent",
"twui-button-accent-outlined",
);
if (color == "gray")
return twMerge(
"bg-transparent outline outline-1 outline-slate-300",
"text-slate-600",
"twui-button-gray-outlined"
"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 outline-none p-2",
"text-blue-500",
"twui-button-primary-ghost"
"bg-transparent dark:bg-transparent outline-none p-2",
"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 outline-none p-2",
"text-emerald-500",
"twui-button-secondary-ghost"
"bg-transparent dark:bg-transparent outline-none p-2",
"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 outline-none p-2",
"text-violet-500",
"twui-button-accent-ghost"
"bg-transparent dark:bg-transparent outline-none p-2",
"text-accent hover:bg-transparent dark:hover:bg-transparent",
"twui-button-accent-ghost",
);
if (color == "gray")
return twMerge(
"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")
return twMerge(
"bg-transparent outline-none p-2",
"text-slate-600",
"twui-button-gray-ghost"
"text-red-600 dark:text-red-400",
"twui-button-error-ghost",
);
if (color == "warning")
return twMerge(
"bg-transparent outline-none p-2",
"text-yellow-600",
"twui-button-warning-ghost",
);
if (color == "success")
return twMerge(
"bg-transparent outline-none p-2",
"text-success",
"twui-button-success-ghost",
);
if (color == "white")
return twMerge(
"bg-transparent outline-none p-2",
"text-white",
"twui-button-white-ghost",
);
}
@@ -127,30 +254,81 @@ 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 twui-button-small text-sm"
: 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,
loading ? "pointer-events-none opacity-80" : "",
props.className,
loading ? "pointer-events-none opacity-80" : "l"
)}
aria-label={props.title}
>
<div
{...buttonContentProps}
className={twMerge(
"flex items-center gap-2",
"flex flex-row items-center gap-2 whitespace-nowrap",
loading ? "opacity-0" : "",
"twui-button-content-wrapper"
"twui-button-content-wrapper",
buttonContentProps?.className,
)}
>
{beforeIcon ? (
typeof beforeIcon == "string" ? (
<LucideIcon name={beforeIcon as TWUILucideIconName} />
) : (
beforeIcon
)
) : null}
{props.children}
{afterIcon ? (
typeof afterIcon == "string" ? (
<LucideIcon name={afterIcon as TWUILucideIconName} />
) : (
afterIcon
)
) : null}
</div>
{loading && <Loading className="absolute" />}
{loading && (
<Loading
size={(() => {
if (loadingIconSize) return loadingIconSize;
switch (size) {
case "small":
return "small";
case "smaller":
return "smaller";
default:
return "normal";
}
})()}
{...loadingProps}
className={twMerge("absolute", loadingProps?.className)}
/>
)}
</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>
);
+23
View File
@@ -0,0 +1,23 @@
import { DetailedHTMLProps, HTMLAttributes } from "react";
import { twMerge } from "tailwind-merge";
/**
* # Flexbox Column
* @className twui-center
*/
export default function Center({
...props
}: DetailedHTMLProps<HTMLAttributes<HTMLDivElement>, HTMLDivElement>) {
return (
<div
{...props}
className={twMerge(
"flex flex-col items-center justify-center gap-4 p-2 w-full",
"h-full twui-center",
props.className
)}
>
{props.children}
</div>
);
}
+2 -2
View File
@@ -12,8 +12,8 @@ export default function Container({
<div
{...props}
className={twMerge(
"flex items-center w-full max-w-[1200px] gap-4 justify-between",
"flex-wrap flex-col xl:flex-row",
"flex w-full max-w-container gap-4 justify-between",
"flex-wrap flex-col xl:flex-row items-start xl:items-center",
"twui-container",
props.className
)}
+13 -6
View File
@@ -1,16 +1,21 @@
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}
@@ -20,6 +25,8 @@ export default function Divider({
? "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(
"mb-4",
"twui-headings twui-heading",
"twui-h5",
props.className
)}
>
{props.children}
</h5>
+1 -1
View File
@@ -12,7 +12,7 @@ export default function HR({
<hr
{...props}
className={twMerge(
"border-slate-200 dark:border-white/20 w-full my-4",
"border-slate-200 dark:border-white/10 w-full my-4",
"twui-hr",
props.className
)}
+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>
);
}
+138
View File
@@ -0,0 +1,138 @@
import _ from "lodash";
import React, { DetailedHTMLProps, ImgHTMLAttributes, ReactNode } from "react";
import { twMerge } from "tailwind-merge";
export type TWUIImageProps = DetailedHTMLProps<
ImgHTMLAttributes<HTMLImageElement>,
HTMLImageElement
> & {
alt: string;
size?: number;
circle?: boolean;
bgImg?: boolean;
backgroundImage?: boolean;
fallbackImageSrc?: string;
srcLight?: string;
srcDark?: string;
imgErrSrc?: string;
imgErrComp?: ReactNode;
imgErrSrcLight?: string;
imgErrSrcDark?: string;
};
/**
* # Image Component
* @className twui-img
*/
export default function Img({
imgErrSrc,
imgErrComp,
imgErrSrcDark,
imgErrSrcLight,
...props
}: TWUIImageProps) {
const width = props.size || props.width;
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",
"bgImg",
"backgroundImage",
"fallbackImageSrc",
"srcLight",
"srcDark",
]);
const interpolatedProps: typeof props = {
...finalProps,
width: width,
height: height,
className: twMerge(
"object-cover",
props.circle && "rounded-full",
props.bgImg || props.backgroundImage
? "absolute top-0 left-0 w-full h-full object-cover z-0"
: "",
"twui-img",
props.className
),
onError: (e) => {
if (props.fallbackImageSrc) {
e.currentTarget.src = props.fallbackImageSrc;
}
props.onError?.(e);
},
style: {
...(props.size
? {
width: `${props.size}px`,
minWidth: `${props.size}px`,
height: `${props.size}px`,
}
: {}),
...props.style,
},
};
if (imageError) {
return (
imgErrComp || (
<img
loading="lazy"
{...interpolatedProps}
src={
imgErrSrc ||
"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}
onError={(e) => {
setImageError(true);
props.onError?.(e);
}}
/>
);
}
+40 -8
View File
@@ -1,29 +1,61 @@
import { AnchorHTMLAttributes, DetailedHTMLProps } from "react";
import { AnchorHTMLAttributes, DetailedHTMLProps, RefAttributes } from "react";
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>;
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
}: DetailedHTMLProps<
AnchorHTMLAttributes<HTMLAnchorElement>,
HTMLAnchorElement
>) {
}: 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",
"border-0 border-b border-blue-300 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 && (
<ArrowUpRight
size={arrowSize}
{...arrowProps}
className={twMerge(
"inline-block ml-1 -mt-[1px]",
arrowProps?.className
)}
/>
)}
</a>
);
}
@@ -0,0 +1,30 @@
import { DetailedHTMLProps, HTMLAttributes } from "react";
import { twMerge } from "tailwind-merge";
export type LoadingRectangleBlockProps = DetailedHTMLProps<
HTMLAttributes<HTMLDivElement>,
HTMLDivElement
>;
/**
* # A loading Rectangle block
* @className twui-loading-rectangle-block
* @className twui-loading-block
*/
export default function LoadingRectangleBlock({
...props
}: LoadingRectangleBlockProps) {
return (
<div
{...props}
className={twMerge(
"flex items-center w-full h-10 animate-pulse bg-slate-200 rounded",
"dark:bg-slate-800",
"twui-loading-rectangle-block twui-loading-block",
props.className,
)}
>
{props.children}
</div>
);
}
+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(
"",
horizontal ? "w-10" : "w-full h-10",
"twui-spacer",
props.className
)}
>
{props.children}
</div>
);
}
+19 -2
View File
@@ -6,12 +6,29 @@ import { twMerge } from "tailwind-merge";
* @className twui-span
*/
export default function Span({
size,
variant,
truncate,
...props
}: DetailedHTMLProps<HTMLAttributes<HTMLHeadingElement>, HTMLHeadingElement>) {
}: DetailedHTMLProps<HTMLAttributes<HTMLHeadingElement>, HTMLHeadingElement> & {
size?: "normal" | "small" | "smaller" | "large" | "larger";
variant?: "normal" | "faded";
truncate?: { lines?: number; width?: number };
}) {
return (
<span
{...props}
className={twMerge("text-base", "twui-span", props.className)}
className={twMerge(
"",
size == "small" && "text-sm",
size == "smaller" && "text-xs",
size == "large" && "text-lg",
size == "larger" && "text-xl",
variant == "faded" && "opacity-50",
truncate ? `` : ``,
"twui-span",
props.className,
)}
>
{props.children}
</span>
+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>
);
}
+21 -5
View File
@@ -1,21 +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;
componentRef?: React.RefObject<HTMLDivElement>;
};
/**
* # Flexbox Column
* @className twui-stack
*/
export default function Stack({
...props
}: DetailedHTMLProps<HTMLAttributes<HTMLDivElement>, HTMLDivElement>) {
export default function Stack({ gap, componentRef, ...props }: Props) {
const finalProps = _.omit(props, "center");
return (
<div
{...props}
{...finalProps}
className={twMerge(
"flex flex-col items-start",
"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>