ui: share tailwind config across packages

This commit is contained in:
Yangshun Tay
2022-10-03 20:33:35 +08:00
parent 5734758f96
commit de33d38e1b
23 changed files with 374 additions and 15 deletions

View File

@ -7,20 +7,30 @@
"main": "./dist/index.js",
"module": "./dist/index.mjs",
"types": "./dist/index.d.ts",
"exports": {
".": "./dist",
"./styles.css": "./dist/styles.css"
},
"files": [
"dist/**"
],
"scripts": {
"build": "tsup src/index.tsx --format esm,cjs --dts --external react",
"build": "tsup src/index.tsx --format esm,cjs --dts --external react && tailwindcss -i ./src/styles.css -o ./dist/styles.css",
"dev": "concurrently \"tsup src/index.tsx --format esm,cjs --dts --external react --watch\" \"tailwindcss -i ./src/styles.css -o ./dist/styles.css --watch\"",
"clean": "rm -rf dist",
"tsc": "tsc",
"dev": "tsup src/index.tsx --format esm,cjs --watch --dts --external react",
"lint": "eslint src/**/*.ts* --fix"
},
"dependencies": {
"clsx": "^1.2.1",
"next": "^12.3.1"
},
"devDependencies": {
"@tih/tailwind-config": "*",
"@tih/tsconfig": "*",
"@types/react": "^18.0.21",
"@types/react-dom": "^18.0.6",
"concurrently": "^7.4.0",
"eslint": "^8.24.0",
"eslint-config-tih": "*",
"prettier-plugin-tailwindcss": "^0.1.13",

View File

@ -0,0 +1,155 @@
import clsx from 'clsx';
import Link from 'next/link';
import type { UrlObject } from 'url';
import Spinner from '../Spinner';
export type ButtonDisplay = 'block' | 'inline';
export type ButtonSize = 'lg' | 'md' | 'sm';
export type ButtonVariant =
| 'primary'
| 'secondary'
| 'special'
| 'success'
| 'tertiary';
type Props = Readonly<{
addonPosition?: 'end' | 'start';
'aria-controls'?: string;
className?: string;
display?: ButtonDisplay;
href?: UrlObject | string;
icon?: (props: React.ComponentProps<'svg'>) => JSX.Element;
isDisabled?: boolean;
isLabelHidden?: boolean;
isLoading?: boolean;
label: string;
onClick?: (event: React.MouseEvent<HTMLElement>) => void;
size?: ButtonSize;
type?: 'button' | 'submit';
variant: ButtonVariant;
}>;
const sizeClasses: Record<ButtonSize, string> = {
lg: 'px-5 py-2.5',
md: 'px-4 py-2',
sm: 'px-2.5 py-1.5',
};
const iconOnlySizeClasses: Record<ButtonSize, string> = {
lg: 'p-3',
md: 'p-2',
sm: 'p-1.5',
};
const baseClasses: Record<ButtonSize, string> = {
lg: 'text-base rounded-xl',
md: 'text-sm rounded-lg',
sm: 'text-xs rounded-md',
};
const sizeIconSpacingEndClasses: Record<ButtonSize, string> = {
lg: 'ml-3 -mr-1 ',
md: 'ml-2 -mr-1 ',
sm: 'ml-2 -mr-0.5',
};
const sizeIconSpacingStartClasses: Record<ButtonSize, string> = {
lg: 'mr-3 -ml-1 ',
md: 'mr-2 -ml-1 ',
sm: 'mr-2 -ml-0.5',
};
const sizeIconClasses: Record<ButtonSize, string> = {
lg: '!h-5 !w-5',
md: '!h-5 !w-5',
sm: '!h-4 !w-4',
};
const variantClasses: Record<ButtonVariant, string> = {
primary: 'border-transparent text-white bg-primary-600 hover:bg-primary-500',
secondary:
'border-transparent text-primary-700 bg-primary-100 hover:bg-primary-200',
special: 'border-slate-900 text-white bg-slate-900 hover:bg-slate-700',
success: 'border-transparent text-white bg-emerald-600 hover:bg-emerald-500',
tertiary: 'border-slate-300 text-slate-700 bg-white hover:bg-slate-50',
};
const variantDisabledClasses: Record<ButtonVariant, string> = {
primary: 'border-transparent text-slate-500 bg-slate-300',
secondary: 'border-transparent text-slate-400 bg-slate-200',
special: 'border-transparent text-slate-500 bg-slate-300',
success: 'border-transparent text-slate-500 bg-slate-300',
tertiary: 'border-slate-300 text-slate-400 bg-slate-100',
};
export default function Button({
addonPosition = 'end',
'aria-controls': ariaControls,
className,
display = 'inline',
href,
icon: Icon,
isDisabled = false,
isLabelHidden = false,
isLoading = false,
label,
size = 'md',
type = 'button',
variant,
onClick,
}: Props) {
const iconSpacingClass = (() => {
if (!isLabelHidden && addonPosition === 'start') {
return sizeIconSpacingStartClasses[size];
}
if (!isLabelHidden && addonPosition === 'end') {
return sizeIconSpacingEndClasses[size];
}
})();
const addOnClass = clsx(iconSpacingClass, sizeIconClasses[size]);
const addOn = isLoading ? (
<Spinner className={addOnClass} color="inherit" size="xs" />
) : Icon != null ? (
<Icon aria-hidden="true" className={addOnClass} />
) : null;
const children = (
<>
{addonPosition === 'start' && addOn}
{!isLabelHidden && label}
{addonPosition === 'end' && addOn}
</>
);
const commonProps = {
'aria-controls': ariaControls ?? undefined,
'aria-label': isLabelHidden ? label : undefined,
children,
className: clsx(
display === 'block' ? 'flex w-full justify-center' : 'inline-flex',
'whitespace-nowrap items-center border font-medium focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-primary-500',
isDisabled ? variantDisabledClasses[variant] : variantClasses[variant],
isDisabled && 'pointer-events-none',
isLabelHidden ? iconOnlySizeClasses[size] : sizeClasses[size],
baseClasses[size],
className,
),
disabled: isDisabled,
onClick,
};
if (href == null) {
return (
<button type={type === 'button' ? 'button' : 'submit'} {...commonProps} />
);
}
return (
<Link href={href}>
<a {...commonProps} />
</Link>
);
}

View File

@ -0,0 +1,4 @@
import Button from './Button';
export * from './Button';
export default Button;

View File

@ -10,7 +10,7 @@ export function CounterButton() {
fontWeight: 500,
padding: '1.5rem',
}}>
<p style={{ margin: '0 0 1.5rem 0' }}>
<p className="text-green-500" style={{ margin: '0 0 1.5rem 0' }}>
This component is from{' '}
<code
style={{

View File

@ -0,0 +1,52 @@
import clsx from 'clsx';
export type SpinnerColor = 'default' | 'inherit';
export type SpinnerSize = 'lg' | 'md' | 'sm' | 'xs';
export type SpinnerDisplay = 'block' | 'inline';
type Props = Readonly<{
className?: string;
color?: SpinnerColor;
display?: SpinnerDisplay;
label?: string;
size: SpinnerSize;
}>;
const colorClasses: Record<SpinnerColor, string> = {
default: 'text-slate-400',
inherit: '',
};
const sizeClasses: Record<SpinnerSize, string> = {
lg: 'w-12 h-12 border-[6px]',
md: 'w-8 h-8 border-4',
sm: 'w-6 h-6 border-[3px]',
xs: 'w-4 h-4 border-2',
};
export default function Spinner({
className,
color = 'default',
display = 'inline',
label = 'Loading...',
size,
}: Props) {
const spinner = (
<div
className={clsx(
'inline-block animate-spin rounded-full border-current border-r-transparent',
colorClasses[color],
sizeClasses[size],
className,
)}
role="status">
<span className="sr-only">{label}</span>
</div>
);
if (display === 'block') {
return <div className="text-center">{spinner}</div>;
}
return spinner;
}

View File

@ -0,0 +1,4 @@
import Spinner from './Spinner';
export * from './Spinner';
export default Spinner;

View File

@ -1,2 +1,4 @@
export { default as Button } from './Button';
export * from './Button';
export { CounterButton } from './CounterButton';
export { NewTabLink } from './NewTabLink';

View File

@ -0,0 +1,3 @@
@tailwind base;
@tailwind components;
@tailwind utilities;

View File

@ -0,0 +1,3 @@
const config = require('@tih/tailwind-config/tailwind.config.js');
module.exports = config;