parent
5734758f96
commit
de33d38e1b
@ -1,6 +1,13 @@
|
||||
// If you want to use other PostCSS plugins, see the following:
|
||||
// https://tailwindcss.com/docs/using-with-preprocessors
|
||||
|
||||
const config = require('@tih/tailwind-config/tailwind.config.js');
|
||||
|
||||
module.exports = {
|
||||
plugins: {
|
||||
tailwindcss: {},
|
||||
// Specifying the config is not necessary in most cases, but it is included
|
||||
// here to share the same config across the entire monorepo
|
||||
tailwindcss: { config },
|
||||
autoprefixer: {},
|
||||
},
|
||||
};
|
||||
|
@ -0,0 +1 @@
|
||||
storybook-static
|
@ -0,0 +1 @@
|
||||
import '@tih/ui/dist/styles.css';
|
@ -0,0 +1,13 @@
|
||||
// If you want to use other PostCSS plugins, see the following:
|
||||
// https://tailwindcss.com/docs/using-with-preprocessors
|
||||
|
||||
const config = require('@tih/tailwind-config/tailwind.config.js');
|
||||
|
||||
module.exports = {
|
||||
plugins: {
|
||||
// Specifying the config is not necessary in most cases, but it is included
|
||||
// here to share the same config across the entire monorepo
|
||||
tailwindcss: { config },
|
||||
autoprefixer: {},
|
||||
},
|
||||
};
|
@ -0,0 +1,33 @@
|
||||
import { ComponentStory, ComponentMeta } from '@storybook/react';
|
||||
|
||||
import { Button } from '@tih/ui';
|
||||
import React from 'react';
|
||||
|
||||
//👇 This default export determines where your story goes in the story list
|
||||
export default {
|
||||
/* 👇 The title prop is optional.
|
||||
* See https://storybook.js.org/docs/react/configure/overview#configure-story-loading
|
||||
* to learn how to generate automatic titles
|
||||
*/
|
||||
title: 'Button',
|
||||
component: Button,
|
||||
} as ComponentMeta<typeof Button>;
|
||||
|
||||
//👇 We create a “template” of how args map to rendering
|
||||
const Template: ComponentStory<typeof Button> = (args) => <Button {...args} />;
|
||||
|
||||
export const PrimaryButton = Template.bind({});
|
||||
|
||||
PrimaryButton.args = {
|
||||
label: 'Button text',
|
||||
size: 'md',
|
||||
variant: 'primary',
|
||||
};
|
||||
|
||||
export const SecondaryButton = Template.bind({});
|
||||
|
||||
SecondaryButton.args = {
|
||||
label: 'Button text',
|
||||
size: 'md',
|
||||
variant: 'secondary',
|
||||
};
|
@ -0,0 +1,13 @@
|
||||
{
|
||||
"name": "@tih/tailwind-config",
|
||||
"version": "0.0.0",
|
||||
"private": true,
|
||||
"main": "index.js",
|
||||
"devDependencies": {
|
||||
"@tailwindcss/aspect-ratio": "^0.4.2",
|
||||
"@tailwindcss/forms": "^0.5.3",
|
||||
"@tailwindcss/line-clamp": "^0.4.2",
|
||||
"@tailwindcss/typography": "^0.5.7",
|
||||
"tailwindcss": "^3.1.8"
|
||||
}
|
||||
}
|
@ -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>
|
||||
);
|
||||
}
|
@ -0,0 +1,4 @@
|
||||
import Button from './Button';
|
||||
|
||||
export * from './Button';
|
||||
export default Button;
|
@ -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;
|
||||
}
|
@ -0,0 +1,4 @@
|
||||
import Spinner from './Spinner';
|
||||
|
||||
export * from './Spinner';
|
||||
export default Spinner;
|
@ -1,2 +1,4 @@
|
||||
export { default as Button } from './Button';
|
||||
export * from './Button';
|
||||
export { CounterButton } from './CounterButton';
|
||||
export { NewTabLink } from './NewTabLink';
|
||||
|
@ -0,0 +1,3 @@
|
||||
@tailwind base;
|
||||
@tailwind components;
|
||||
@tailwind utilities;
|
@ -0,0 +1,3 @@
|
||||
const config = require('@tih/tailwind-config/tailwind.config.js');
|
||||
|
||||
module.exports = config;
|
Loading…
Reference in new issue