Package library as @moduletrace/twui npm package
- Move components/ to src/ and compile to dist/ with tsc - Add root barrel export plus deep-import subpath exports and base.css export - Post-compile step: copy base.css, add .js import extensions, mark client modules - Add publish.sh and .npmrc for the Gitea npm registry - Fix type errors, guard TinyMCE env access, drop ace-builds type dependency - Rename clashing props type names, remove Next.js scaffolding and unused deps Co-Authored-By: Claude Opus 5.5 <[email protected]>
This commit is contained in:
@@ -0,0 +1,367 @@
|
||||
import React, {
|
||||
type AnchorHTMLAttributes,
|
||||
type ButtonHTMLAttributes,
|
||||
type ComponentProps,
|
||||
type DetailedHTMLProps,
|
||||
type HTMLAttributeAnchorTarget,
|
||||
type HTMLAttributes,
|
||||
} from "react";
|
||||
import { twMerge } from "tailwind-merge";
|
||||
import Loading from "../elements/Loading";
|
||||
import LucideIcon, { type TWUILucideIconName } from "../elements/lucide-icon";
|
||||
import type { LucideProps } from "lucide-react";
|
||||
|
||||
export const TWUIButtonColors = [
|
||||
"primary",
|
||||
"secondary",
|
||||
"text",
|
||||
"white",
|
||||
"accent",
|
||||
"gray",
|
||||
"error",
|
||||
"warning",
|
||||
"success",
|
||||
] as const;
|
||||
|
||||
export const TWUIButtonSizes = [
|
||||
"small",
|
||||
"smaller",
|
||||
"normal",
|
||||
"large",
|
||||
"larger",
|
||||
] as const;
|
||||
|
||||
export const TWUIButtonVariants = ["normal", "ghost", "outlined"] as const;
|
||||
|
||||
export type TWUIButtonColorType = (typeof TWUIButtonColors)[number];
|
||||
export type TWUIButtonSizeType = (typeof TWUIButtonSizes)[number];
|
||||
export type TWUIButtonVariantType = (typeof TWUIButtonVariants)[number];
|
||||
|
||||
export type TWUIButtonProps = DetailedHTMLProps<
|
||||
ButtonHTMLAttributes<HTMLButtonElement>,
|
||||
HTMLButtonElement
|
||||
> & {
|
||||
title: string;
|
||||
variant?: TWUIButtonVariantType;
|
||||
color?: TWUIButtonColorType;
|
||||
size?: TWUIButtonSizeType;
|
||||
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;
|
||||
before_icon_name?: TWUILucideIconName;
|
||||
after_icon_name?: TWUILucideIconName;
|
||||
before_icon_props?: LucideProps;
|
||||
after_icon_props?: LucideProps;
|
||||
buttonContentProps?: DetailedHTMLProps<
|
||||
HTMLAttributes<HTMLDivElement>,
|
||||
HTMLDivElement
|
||||
>;
|
||||
loadingProps?: ComponentProps<typeof Loading>;
|
||||
};
|
||||
|
||||
export const ButtonClasses: Record<string, Record<string, string>> = {
|
||||
normal: {
|
||||
primary: twMerge(
|
||||
"bg-primary hover:bg-primary-hover text-white",
|
||||
"dark:bg-primary-dark hover:dark:bg-primary-dark-hover",
|
||||
"twui-button-primary",
|
||||
),
|
||||
secondary: twMerge(
|
||||
"bg-secondary hover:bg-secondary-hover text-white",
|
||||
"dark:bg-secondary-dark hover:dark:bg-secondary-dark-hover dark:text-secondary-dark-text",
|
||||
"twui-button-secondary",
|
||||
),
|
||||
white: twMerge(
|
||||
"!bg-white hover:!bg-slate-200 !text-slate-800",
|
||||
"twui-button-white",
|
||||
),
|
||||
accent: twMerge(
|
||||
"bg-accent hover:bg-accent-hover text-white",
|
||||
"dark:bg-accent-dark hover:dark:bg-accent-dark-hover dark:text-accent-dark-text",
|
||||
"twui-button-accent",
|
||||
),
|
||||
gray: twMerge(
|
||||
"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",
|
||||
),
|
||||
success: twMerge(
|
||||
"bg-success hover:bg-success-hover text-white",
|
||||
"dark:bg-success-dark-fill hover:dark:bg-success-dark-fill-hover dark:text-success-dark-text",
|
||||
"twui-button-success",
|
||||
),
|
||||
error: twMerge(
|
||||
"bg-error hover:bg-error-hover text-white",
|
||||
"dark:bg-error-dark-fill hover:dark:bg-error-dark-fill-hover dark:text-error-dark-text",
|
||||
"twui-button-error",
|
||||
),
|
||||
warning: twMerge(
|
||||
"bg-warning hover:bg-warning-hover text-white",
|
||||
"dark:bg-warning-dark-fill hover:dark:bg-warning-dark-fill-hover dark:text-warning-dark-text",
|
||||
"twui-button-warning",
|
||||
),
|
||||
},
|
||||
outlined: {
|
||||
primary: twMerge(
|
||||
"bg-transparent outline outline-1 outline-primary-outline",
|
||||
"text-primary-text dark:text-primary-dark-text dark:outline-primary-dark-outline",
|
||||
"twui-button-primary-outlined",
|
||||
),
|
||||
secondary: twMerge(
|
||||
"bg-transparent outline outline-1 outline-secondary",
|
||||
"text-secondary dark:text-secondary-dark dark:outline-secondary-dark-outline",
|
||||
"twui-button-secondary-outlined",
|
||||
),
|
||||
accent: twMerge(
|
||||
"bg-transparent outline outline-1 outline-accent",
|
||||
"text-accent",
|
||||
"twui-button-accent-outlined",
|
||||
),
|
||||
gray: twMerge(
|
||||
"bg-transparent outline outline-1 outline-gray",
|
||||
"text-dark dark:text-white/60 dark:outline-white/30",
|
||||
"twui-button-gray-outlined",
|
||||
),
|
||||
white: twMerge(
|
||||
"bg-transparent outline outline-1 outline-white/50",
|
||||
"text-white",
|
||||
"twui-button-white-outlined",
|
||||
),
|
||||
error: twMerge(
|
||||
"bg-transparent outline outline-1 outline-error text-error",
|
||||
"dark:outline-error-dark dark:text-error-dark",
|
||||
"twui-button-error-outlined",
|
||||
),
|
||||
warning: twMerge(
|
||||
"bg-transparent outline outline-1 outline-warning text-warning",
|
||||
"dark:outline-warning-dark dark:text-warning-dark",
|
||||
"twui-button-warning-outlined",
|
||||
),
|
||||
success: twMerge(
|
||||
"bg-transparent outline outline-1 outline-success text-success",
|
||||
"dark:outline-success-dark dark:text-success-dark",
|
||||
"twui-button-success-outlined",
|
||||
),
|
||||
},
|
||||
ghost: {
|
||||
primary: twMerge(
|
||||
"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",
|
||||
),
|
||||
secondary: twMerge(
|
||||
"bg-transparent dark:bg-transparent outline-none p-2",
|
||||
"text-secondary dark:text-secondary-dark hover:bg-transparent dark:hover:bg-transparent",
|
||||
"twui-button-secondary-ghost",
|
||||
),
|
||||
text: 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",
|
||||
),
|
||||
accent: twMerge(
|
||||
"bg-transparent dark:bg-transparent outline-none p-2",
|
||||
"text-accent hover:bg-transparent dark:hover:bg-transparent",
|
||||
"twui-button-accent-ghost",
|
||||
),
|
||||
gray: 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",
|
||||
),
|
||||
error: twMerge(
|
||||
"bg-transparent outline-none p-2",
|
||||
"text-error dark:text-error-dark",
|
||||
"twui-button-error-ghost",
|
||||
),
|
||||
warning: twMerge(
|
||||
"bg-transparent outline-none p-2",
|
||||
"text-warning dark:text-warning-dark",
|
||||
"twui-button-warning-ghost",
|
||||
),
|
||||
success: twMerge(
|
||||
"bg-transparent outline-none p-2",
|
||||
"text-success dark:text-success-dark",
|
||||
"twui-button-success-ghost",
|
||||
),
|
||||
white: twMerge(
|
||||
"bg-transparent outline-none p-2",
|
||||
"text-white",
|
||||
"twui-button-white-ghost",
|
||||
),
|
||||
},
|
||||
};
|
||||
|
||||
/**
|
||||
* # Buttons
|
||||
* @className twui-button-general
|
||||
* @className twui-button-content-wrapper
|
||||
* @className twui-button-primary
|
||||
* @className twui-button-primary-outlined
|
||||
* @className twui-button-primary-ghost
|
||||
* @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,
|
||||
before_icon_name,
|
||||
after_icon_name,
|
||||
before_icon_props,
|
||||
after_icon_props,
|
||||
...props
|
||||
}: TWUIButtonProps) {
|
||||
const finalClassName =
|
||||
ButtonClasses[variant || "normal"]?.[color || "primary"] ?? "";
|
||||
|
||||
const buttonComponent = (
|
||||
<button
|
||||
{...props}
|
||||
className={twMerge(
|
||||
"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"
|
||||
: "text-sm twui-button-base",
|
||||
finalClassName,
|
||||
loading ? "pointer-events-none opacity-80" : "",
|
||||
props.className,
|
||||
)}
|
||||
aria-label={props.title}
|
||||
>
|
||||
<div
|
||||
{...buttonContentProps}
|
||||
className={twMerge(
|
||||
"flex flex-row items-center gap-2 whitespace-nowrap",
|
||||
loading ? "opacity-0" : "",
|
||||
"twui-button-content-wrapper",
|
||||
buttonContentProps?.className,
|
||||
)}
|
||||
>
|
||||
{before_icon_name ? (
|
||||
<LucideIcon
|
||||
{...before_icon_props}
|
||||
name={before_icon_name}
|
||||
/>
|
||||
) : beforeIcon ? (
|
||||
typeof beforeIcon == "string" ? (
|
||||
<LucideIcon name={beforeIcon as TWUILucideIconName} />
|
||||
) : (
|
||||
beforeIcon
|
||||
)
|
||||
) : null}
|
||||
{props.children}
|
||||
{after_icon_name ? (
|
||||
<LucideIcon {...after_icon_props} name={after_icon_name} />
|
||||
) : afterIcon ? (
|
||||
typeof afterIcon == "string" ? (
|
||||
<LucideIcon name={afterIcon as TWUILucideIconName} />
|
||||
) : (
|
||||
afterIcon
|
||||
)
|
||||
) : null}
|
||||
</div>
|
||||
|
||||
{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}
|
||||
title={props.title}
|
||||
aria-label={props.title}
|
||||
>
|
||||
{buttonComponent}
|
||||
</a>
|
||||
);
|
||||
return buttonComponent;
|
||||
}
|
||||
Reference in New Issue
Block a user