first commit
Some checks failed
Build / run (push) Has been cancelled

This commit is contained in:
maher
2025-10-29 11:42:25 +01:00
commit 703f50a09d
4595 changed files with 385164 additions and 0 deletions

View File

@@ -0,0 +1,96 @@
import React, {
ComponentPropsWithRef,
forwardRef,
JSXElementConstructor,
} from 'react';
import clsx from 'clsx';
import {RelativeRoutingType, To} from 'react-router-dom';
import {
ButtonColor,
ButtonVariant,
getSharedButtonStyle,
} from './get-shared-button-style';
import {createEventHandler} from '../../utils/dom/create-event-handler';
export interface ButtonBaseProps
extends Omit<ComponentPropsWithRef<'button'>, 'color'> {
color?: ButtonColor;
variant?: ButtonVariant;
value?: any;
justify?: string;
display?: string;
radius?: string;
shadow?: string;
border?: string;
whitespace?: string;
form?: string;
to?: To;
relative?: RelativeRoutingType;
href?: string;
target?: '_blank';
rel?: string;
replace?: boolean;
end?: boolean;
elementType?: 'button' | 'a' | JSXElementConstructor<any>;
download?: boolean | string;
}
export const ButtonBase = forwardRef<
HTMLButtonElement | HTMLLinkElement,
ButtonBaseProps
>((props, ref) => {
const {
children,
color = null,
variant,
radius,
shadow,
whitespace,
justify = 'justify-center',
className,
href,
form,
border,
elementType,
to,
relative,
replace,
end,
display,
type = 'button',
onClick,
onPointerDown,
onPointerUp,
onKeyDown,
...domProps
} = props;
const Element = elementType || (href ? 'a' : 'button');
const isLink = Element === 'a';
return (
<Element
ref={ref as any}
form={isLink ? undefined : form}
href={href}
to={to}
relative={relative}
type={isLink ? undefined : type}
replace={replace}
end={end}
onPointerDown={createEventHandler(onPointerDown)}
onPointerUp={createEventHandler(onPointerUp)}
onClick={createEventHandler(onClick)}
onKeyDown={createEventHandler(onKeyDown)}
className={clsx(
'focus-visible:ring',
getSharedButtonStyle({variant, color, border, whitespace, display}),
radius,
justify,
className,
)}
{...domProps}
>
{children}
</Element>
);
});

View File

@@ -0,0 +1,111 @@
import React from 'react';
import clsx from 'clsx';
import {ButtonColor, ButtonVariant} from './get-shared-button-style';
import {ButtonProps} from './button';
import {ButtonSize} from './button-size';
export interface ButtonGroupProps {
children: React.ReactNode[];
color?: ButtonColor;
variant?: ButtonVariant;
size?: ButtonSize;
radius?: string;
className?: string;
value?: any;
onChange?: (newValue: any) => void;
multiple?: boolean;
disabled?: boolean;
}
export function ButtonGroup({
children,
color,
variant,
radius = 'rounded-button',
size,
className,
value,
onChange,
multiple,
disabled,
}: ButtonGroupProps) {
const isActive = (childValue: any): boolean => {
// assume that button group is not used as a toggle group, if there is no value given
if (value === undefined) return false;
if (multiple) {
return (value as any[]).includes(childValue);
}
return childValue === value;
};
const toggleMultipleValue = (childValue: any) => {
const newValue = [...value];
const childIndex = value.indexOf(childValue);
if (childIndex > -1) {
newValue.splice(childIndex, 1);
} else {
newValue.push(childValue);
}
return newValue;
};
const buttons = React.Children.map(children, (button, i) => {
if (React.isValidElement(button)) {
const active = isActive(button.props.value);
const adjustedColor = active ? 'primary' : color;
return React.cloneElement<ButtonProps>(button as any, {
color: active ? 'primary' : color,
variant,
size,
radius: null,
disabled: button.props.disabled || disabled,
...button.props,
onClick: e => {
if (button.props.onClick) {
button.props.onClick(e);
}
if (!onChange) return;
if (multiple) {
onChange?.(toggleMultipleValue(button.props.value));
} else {
onChange?.(button.props.value);
}
},
className: clsx(
button.props.className,
// borders are hidden via negative margin, make sure both are visible for active item
active ? 'z-20' : 'z-10',
getStyle(i, children, radius, adjustedColor),
),
});
}
});
return (
<div className={clsx(radius, 'isolate inline-flex', className)}>
{buttons}
</div>
);
}
function getStyle(
i: number,
children: ButtonGroupProps['children'],
radius: ButtonGroupProps['radius'],
color?: ButtonColor,
): string {
// first
if (i === 0) {
return clsx(
radius,
'rounded-tr-none rounded-br-none',
!color && 'border-r-transparent disabled:border-r-transparent',
);
}
// last
if (i === children.length - 1) {
return clsx(radius, 'rounded-tl-none rounded-bl-none -ml-1');
}
return clsx(
'rounded-none -ml-1',
!color && 'border-r-transparent disabled:border-r-transparent',
);
}

View File

@@ -0,0 +1,37 @@
import {ButtonVariant} from './get-shared-button-style';
export type ButtonSize = '2xs' | 'xs' | 'sm' | 'md' | 'lg' | 'xl' | null;
interface Props {
padding?: string;
equalWidth?: boolean;
variant?: ButtonVariant;
}
export function getButtonSizeStyle(
size?: ButtonSize,
{padding, equalWidth, variant}: Props = {}
): string {
switch (size) {
case '2xs':
if (variant === 'link') return 'text-xs';
return `text-xs h-24 ${equalWidth ? 'w-24' : padding || 'px-10'}`;
case 'xs':
if (variant === 'link') return 'text-xs';
return `text-xs h-30 ${equalWidth ? 'w-30' : padding || 'px-14'}`;
case 'sm':
if (variant === 'link') return 'text-sm';
return `text-sm h-36 ${equalWidth ? 'w-36' : padding || 'px-18'}`;
case 'md':
if (variant === 'link') return 'text-base';
return `text-base h-42 ${equalWidth ? 'w-42' : padding || 'px-22'}`;
case 'lg':
if (variant === 'link') return 'text-lg';
return `text-base h-50 ${equalWidth ? 'w-50' : padding || 'px-26'}`;
case 'xl':
if (variant === 'link') return 'text-xl';
return `text-lg h-60 ${equalWidth ? 'w-60' : padding || 'px-32'}`;
default:
return size || '';
}
}

View File

@@ -0,0 +1,78 @@
import React, {ReactElement} from 'react';
import clsx from 'clsx';
import {ButtonSize, getButtonSizeStyle} from './button-size';
import {ButtonBase, ButtonBaseProps} from './button-base';
import {IconSize} from '../../icons/svg-icon';
export interface ButtonProps extends ButtonBaseProps {
size?: ButtonSize;
sizeClassName?: string;
equalWidth?: boolean;
startIcon?: ReactElement | null | false;
endIcon?: ReactElement | null | false;
}
export const Button = React.forwardRef<HTMLButtonElement, ButtonProps>(
(
{
children,
startIcon,
endIcon,
size = 'sm',
sizeClassName,
className,
equalWidth = false,
radius = 'rounded-button',
variant = 'text',
disabled,
elementType,
to,
href,
download,
...other
},
ref,
) => {
const mergedClassName = clsx(
'font-semibold',
sizeClassName || getButtonSizeStyle(size, {equalWidth, variant}),
className,
);
return (
<ButtonBase
className={mergedClassName}
ref={ref}
radius={radius}
variant={variant}
disabled={disabled}
to={disabled ? undefined : to}
href={disabled ? undefined : href}
download={disabled ? undefined : download}
elementType={disabled ? undefined : elementType}
{...other}
>
{startIcon && (
<InlineIcon position="start" icon={startIcon} size={size} />
)}
{children}
{endIcon && <InlineIcon position="end" icon={endIcon} size={size} />}
</ButtonBase>
);
},
);
type InlineIconProps = {
icon: ReactElement;
position: 'start' | 'end';
size?: IconSize | null;
};
function InlineIcon({icon, position, size}: InlineIconProps): ReactElement {
const className = clsx(
'm-auto',
{
'-ml-4 mr-8': position === 'start',
'-mr-4 ml-8': position === 'end',
},
icon.props.className,
);
return React.cloneElement(icon, {className, size});
}

View File

@@ -0,0 +1,18 @@
import {ComponentPropsWithRef} from 'react';
export const LinkStyle =
'text-link hover:underline hover:text-primary-dark focus-visible:ring focus-visible:ring-2 focus-visible:ring-offset-2 outline-none rounded transition-colors';
interface ExternalLinkProps extends ComponentPropsWithRef<'a'> {}
export function ExternalLink({
children,
className,
target = '_blank',
...domProps
}: ExternalLinkProps) {
return (
<a className={LinkStyle} target={target} {...domProps}>
{children}
</a>
);
}

View File

@@ -0,0 +1,172 @@
export type ButtonVariant =
| 'text'
| 'flat'
| 'raised'
| 'outline'
| 'link'
| null;
export type ButtonColor =
| null
| 'primary'
| 'danger'
| 'positive'
| 'paper'
| 'chip'
| 'white';
interface SharedButtonStyleProps {
variant?: ButtonVariant;
color?: ButtonColor;
border?: string;
shadow?: string;
whitespace?: string;
display?: string;
}
export function getSharedButtonStyle(
props: SharedButtonStyleProps,
): (string | boolean | null | undefined)[] {
const {
variant,
shadow,
whitespace = 'whitespace-nowrap',
display = 'inline-flex',
} = props;
const variantProps = {...props, border: props.border || 'border'};
let style: string[] = [];
if (variant === 'outline') {
style = outline(variantProps);
} else if (variant === 'text') {
style = text(variantProps);
} else if (variant === 'flat' || variant === 'raised') {
style = contained(variantProps);
} else if (variant === 'link') {
style = link(variantProps);
}
return [
...style,
shadow || (variant === 'raised' && 'shadow-md'),
whitespace,
display,
variant &&
'align-middle flex-shrink-0 items-center transition-button duration-200',
'select-none appearance-none no-underline outline-none disabled:pointer-events-none disabled:cursor-default',
];
}
function outline({color, border}: SharedButtonStyleProps) {
const disabled =
'disabled:text-disabled disabled:bg-transparent disabled:border-disabled-bg';
switch (color) {
case 'primary':
return [
`text-primary bg-transparent ${border} border-primary/50`,
'hover:bg-primary/hover hover:border-primary',
disabled,
];
case 'danger':
return [
`text-danger bg-transparent ${border} border-danger/50`,
'hover:bg-danger/4 hover:border-danger',
disabled,
];
case 'positive':
return [
`text-positive bg-transparent ${border} border-positive/50`,
'hover:bg-positive/4 hover:border-positive',
disabled,
];
case 'paper':
return [`text bg-paper ${border}`, 'hover:bg-hover', disabled];
case 'white':
return [
'text-white bg-transparent border border-white',
'hover:bg-white/20',
'disabled:text-white/70 disabled:border-white/70 disabled:bg-transparent',
];
default:
return [`bg-transparent ${border}`, 'hover:bg-hover', disabled];
}
}
function text({color}: SharedButtonStyleProps) {
const disabled = 'disabled:text-disabled disabled:bg-transparent';
switch (color) {
case 'primary':
return [
'text-primary bg-transparent border-transparent',
'hover:bg-primary/4',
disabled,
];
case 'danger':
return [
'text-danger bg-transparent border-transparent',
'hover:bg-danger/4',
disabled,
];
case 'positive':
return [
'text-positive bg-transparent border-transparent',
'hover:bg-positive/4',
disabled,
];
case 'white':
return [
'text-white bg-transparent border-transparent',
'hover:bg-white/20',
'disabled:text-white/70 disabled:bg-transparent',
];
default:
return ['bg-transparent border-transparent', 'hover:bg-hover', disabled];
}
}
function link({color}: SharedButtonStyleProps) {
switch (color) {
case 'primary':
return ['text-primary', 'hover:underline', 'disabled:text-disabled'];
case 'danger':
return ['text-danger', 'hover:underline', 'disabled:text-disabled'];
default:
return ['text-main', 'hover:underline', 'disabled:text-disabled'];
}
}
function contained({color, border}: SharedButtonStyleProps) {
const disabled =
'disabled:text-disabled disabled:bg-disabled disabled:border-transparent disabled:shadow-none';
switch (color) {
case 'primary':
return [
`text-on-primary bg-primary ${border} border-primary`,
'hover:bg-primary-dark hover:border-primary-dark',
disabled,
];
case 'danger':
return [
`text-white bg-danger ${border} border-danger`,
'hover:bg-danger/90 hover:border-danger/90',
disabled,
];
case 'chip':
return [
`text-main bg-chip ${border} border-chip`,
'hover:bg-chip/90 hover:border-chip/90',
disabled,
];
case 'paper':
return [
`text-main bg-paper ${border} border-paper`,
'hover:bg-paper/90 hover:border-paper/90',
disabled,
];
case 'white':
return [
`text-black bg-white ${border} border-white`,
'hover:bg-white',
disabled,
];
default:
return [`bg ${border} border-background`, 'hover:bg-hover', disabled];
}
}

View File

@@ -0,0 +1,51 @@
import React, {cloneElement, forwardRef, ReactElement} from 'react';
import clsx from 'clsx';
import {ButtonSize, getButtonSizeStyle} from './button-size';
import {ButtonBase, ButtonBaseProps} from './button-base';
import {BadgeProps} from '@common/ui/badge/badge';
export interface IconButtonProps extends ButtonBaseProps {
children: ReactElement;
padding?: string;
size?: ButtonSize | null;
iconSize?: ButtonSize | null;
equalWidth?: boolean;
badge?: ReactElement<BadgeProps>;
}
export const IconButton = forwardRef<HTMLButtonElement, IconButtonProps>(
(
{
children,
size = 'md',
// only set icon size based on button size if "ButtonSize" is passed in and not custom className
iconSize = size && size.length <= 3 ? size : 'md',
variant = 'text',
radius = 'rounded-button',
className,
padding,
equalWidth = true,
badge,
...other
},
ref,
) => {
const mergedClassName = clsx(
getButtonSizeStyle(size, {padding, equalWidth, variant}),
className,
badge && 'relative',
);
return (
<ButtonBase
{...other}
ref={ref}
radius={radius}
variant={variant}
className={mergedClassName}
>
{cloneElement(children, {size: iconSize})}
{badge}
</ButtonBase>
);
},
);