mirror of
https://github.com/yangshun/tech-interview-handbook.git
synced 2025-07-14 18:05:55 +08:00
ui: add components
This commit is contained in:
@ -22,6 +22,8 @@
|
||||
"lint": "eslint src/**/*.ts* --fix"
|
||||
},
|
||||
"dependencies": {
|
||||
"@headlessui/react": "^1.7.3",
|
||||
"@heroicons/react": "^2.0.11",
|
||||
"clsx": "^1.2.1",
|
||||
"next": "^12.3.1"
|
||||
},
|
||||
|
57
packages/ui/src/Badge/Badge.tsx
Normal file
57
packages/ui/src/Badge/Badge.tsx
Normal file
@ -0,0 +1,57 @@
|
||||
import clsx from 'clsx';
|
||||
|
||||
export type BadgeVariant =
|
||||
| 'danger'
|
||||
| 'info'
|
||||
| 'primary'
|
||||
| 'success'
|
||||
| 'warning';
|
||||
|
||||
type Props = Readonly<{
|
||||
label: string;
|
||||
variant: BadgeVariant;
|
||||
}>;
|
||||
|
||||
const classes: Record<
|
||||
BadgeVariant,
|
||||
Readonly<{
|
||||
backgroundClass: string;
|
||||
textClass: string;
|
||||
}>
|
||||
> = {
|
||||
danger: {
|
||||
backgroundClass: 'bg-danger-100',
|
||||
textClass: 'text-danger-800',
|
||||
},
|
||||
info: {
|
||||
backgroundClass: 'bg-info-100',
|
||||
textClass: 'text-info-800',
|
||||
},
|
||||
primary: {
|
||||
backgroundClass: 'bg-primary-100',
|
||||
textClass: 'text-primary-800',
|
||||
},
|
||||
success: {
|
||||
backgroundClass: 'bg-success-100',
|
||||
textClass: 'text-success-800',
|
||||
},
|
||||
warning: {
|
||||
backgroundClass: 'bg-warning-100',
|
||||
textClass: 'text-warning-800',
|
||||
},
|
||||
};
|
||||
|
||||
export default function Badge({ label, variant }: Props) {
|
||||
const { backgroundClass, textClass } = classes[variant];
|
||||
|
||||
return (
|
||||
<span
|
||||
className={clsx(
|
||||
'inline-flex items-center rounded-full px-3 py-1 text-xs font-medium',
|
||||
backgroundClass,
|
||||
textClass,
|
||||
)}>
|
||||
{label}
|
||||
</span>
|
||||
);
|
||||
}
|
@ -73,7 +73,7 @@ const variantClasses: Record<ButtonVariant, string> = {
|
||||
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',
|
||||
success: 'border-transparent text-white bg-success-600 hover:bg-success-500',
|
||||
tertiary: 'border-slate-300 text-slate-700 bg-white hover:bg-slate-50',
|
||||
};
|
||||
|
||||
@ -150,8 +150,7 @@ export default function Button({
|
||||
}
|
||||
|
||||
return (
|
||||
<Link href={href}>
|
||||
<a {...commonProps} />
|
||||
</Link>
|
||||
// TODO: Allow passing in of Link component.
|
||||
<Link href={href} {...commonProps} />
|
||||
);
|
||||
}
|
||||
|
89
packages/ui/src/Dialog/Dialog.tsx
Normal file
89
packages/ui/src/Dialog/Dialog.tsx
Normal file
@ -0,0 +1,89 @@
|
||||
import clsx from 'clsx';
|
||||
import { Fragment, useRef } from 'react';
|
||||
import { Dialog as HeadlessDialog, Transition } from '@headlessui/react';
|
||||
|
||||
type Props = Readonly<{
|
||||
children: React.ReactNode;
|
||||
isShown?: boolean;
|
||||
onClose: () => void;
|
||||
primaryButton: React.ReactNode;
|
||||
secondaryButton?: React.ReactNode;
|
||||
title: string;
|
||||
topIcon?: (props: React.ComponentProps<'svg'>) => JSX.Element;
|
||||
}>;
|
||||
|
||||
export default function Dialog({
|
||||
children,
|
||||
isShown,
|
||||
primaryButton,
|
||||
title,
|
||||
topIcon: TopIcon,
|
||||
secondaryButton,
|
||||
onClose,
|
||||
}: Props) {
|
||||
const cancelButtonRef = useRef(null);
|
||||
|
||||
return (
|
||||
<Transition.Root as={Fragment} show={isShown}>
|
||||
<HeadlessDialog
|
||||
as="div"
|
||||
className="relative z-10"
|
||||
initialFocus={cancelButtonRef}
|
||||
onClose={() => onClose()}>
|
||||
<Transition.Child
|
||||
as={Fragment}
|
||||
enter="ease-out duration-300"
|
||||
enterFrom="opacity-0"
|
||||
enterTo="opacity-100"
|
||||
leave="ease-in duration-200"
|
||||
leaveFrom="opacity-100"
|
||||
leaveTo="opacity-0">
|
||||
<div className="fixed inset-0 bg-slate-500 bg-opacity-75 transition-opacity" />
|
||||
</Transition.Child>
|
||||
<div className="fixed inset-0 z-10 overflow-y-auto">
|
||||
<div className="flex min-h-full items-end justify-center p-4 sm:items-center sm:p-0">
|
||||
<Transition.Child
|
||||
as={Fragment}
|
||||
enter="ease-out duration-300"
|
||||
enterFrom="opacity-0 translate-y-4 sm:translate-y-0 sm:scale-95"
|
||||
enterTo="opacity-100 translate-y-0 sm:scale-100"
|
||||
leave="ease-in duration-200"
|
||||
leaveFrom="opacity-100 translate-y-0 sm:scale-100"
|
||||
leaveTo="opacity-0 translate-y-4 sm:translate-y-0 sm:scale-95">
|
||||
<HeadlessDialog.Panel className="relative transform overflow-hidden rounded-lg bg-white px-4 pt-5 pb-4 text-left shadow-xl transition-all sm:my-8 sm:w-full sm:max-w-lg sm:p-6">
|
||||
<div>
|
||||
{TopIcon != null && (
|
||||
<div className="mx-auto flex h-12 w-12 items-center justify-center rounded-full bg-green-100">
|
||||
<TopIcon
|
||||
aria-hidden="true"
|
||||
className="h-6 w-6 text-green-600"
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
<div>
|
||||
<HeadlessDialog.Title
|
||||
as="h2"
|
||||
className="text-2xl font-bold leading-6 text-slate-900">
|
||||
{title}
|
||||
</HeadlessDialog.Title>
|
||||
<div className="my-4">
|
||||
<div className="text-sm">{children}</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
className={clsx(
|
||||
'mt-5 grid gap-3 sm:mt-6 sm:grid-flow-row-dense',
|
||||
secondaryButton != null && 'sm:grid-cols-2',
|
||||
)}>
|
||||
{secondaryButton}
|
||||
{primaryButton}
|
||||
</div>
|
||||
</HeadlessDialog.Panel>
|
||||
</Transition.Child>
|
||||
</div>
|
||||
</div>
|
||||
</HeadlessDialog>
|
||||
</Transition.Root>
|
||||
);
|
||||
}
|
64
packages/ui/src/DropdownMenu/DropdownMenu.tsx
Normal file
64
packages/ui/src/DropdownMenu/DropdownMenu.tsx
Normal file
@ -0,0 +1,64 @@
|
||||
import clsx from 'clsx';
|
||||
import React, { Fragment } from 'react';
|
||||
import { Menu, Transition } from '@headlessui/react';
|
||||
import { ChevronDownIcon } from '@heroicons/react/24/solid';
|
||||
|
||||
import DropdownMenuItem from './DropdownMenuItem';
|
||||
|
||||
export type DropdownMenuAlignment = 'end' | 'start';
|
||||
export type DropdownMenuSize = 'inherit' | 'regular';
|
||||
|
||||
type Props = Readonly<{
|
||||
align?: DropdownMenuAlignment;
|
||||
children: React.ReactNode; // TODO: Change to strict children.
|
||||
label: React.ReactNode;
|
||||
size?: DropdownMenuSize;
|
||||
}>;
|
||||
|
||||
DropdownMenu.Item = DropdownMenuItem;
|
||||
|
||||
const alignmentClasses: Record<DropdownMenuAlignment, string> = {
|
||||
end: 'origin-top-right right-0',
|
||||
start: 'origin-top-left left-0',
|
||||
};
|
||||
|
||||
export default function DropdownMenu({
|
||||
align = 'start',
|
||||
children,
|
||||
label,
|
||||
size = 'regular',
|
||||
}: Props) {
|
||||
return (
|
||||
<Menu as="div" className="relative inline-block">
|
||||
<div className="flex">
|
||||
<Menu.Button
|
||||
className={clsx(
|
||||
'group inline-flex justify-center font-medium text-slate-700 hover:text-slate-900',
|
||||
size === 'regular' && 'text-sm',
|
||||
)}>
|
||||
<div>{label}</div>
|
||||
<ChevronDownIcon
|
||||
aria-hidden="true"
|
||||
className="-mr-1 ml-1 h-5 w-5 flex-shrink-0 text-slate-400 group-hover:text-slate-500"
|
||||
/>
|
||||
</Menu.Button>
|
||||
</div>
|
||||
<Transition
|
||||
as={Fragment}
|
||||
enter="transition ease-out duration-100"
|
||||
enterFrom="transform opacity-0 scale-95"
|
||||
enterTo="transform opacity-100 scale-100"
|
||||
leave="transition ease-in duration-75"
|
||||
leaveFrom="transform opacity-100 scale-100"
|
||||
leaveTo="transform opacity-0 scale-95">
|
||||
<Menu.Items
|
||||
className={clsx(
|
||||
alignmentClasses[align],
|
||||
'ring-primary-500 absolute z-10 mt-2 w-48 rounded-md bg-white shadow-lg ring-1 ring-opacity-5 focus:outline-none',
|
||||
)}>
|
||||
<div className="py-1">{children}</div>
|
||||
</Menu.Items>
|
||||
</Transition>
|
||||
</Menu>
|
||||
);
|
||||
}
|
41
packages/ui/src/DropdownMenu/DropdownMenuItem.tsx
Normal file
41
packages/ui/src/DropdownMenu/DropdownMenuItem.tsx
Normal file
@ -0,0 +1,41 @@
|
||||
import clsx from 'clsx';
|
||||
import React from 'react';
|
||||
|
||||
import { Menu } from '@headlessui/react';
|
||||
|
||||
type Props = Readonly<{
|
||||
href?: string;
|
||||
isSelected?: boolean;
|
||||
label: React.ReactNode;
|
||||
onClick: () => void;
|
||||
}>;
|
||||
|
||||
export default function DropdownMenuItem({
|
||||
href,
|
||||
isSelected = false,
|
||||
label,
|
||||
onClick,
|
||||
}: Props) {
|
||||
return (
|
||||
<Menu.Item>
|
||||
{({ active }) => {
|
||||
const props = {
|
||||
children: label,
|
||||
className: clsx(
|
||||
isSelected ? 'font-medium text-slate-900' : 'text-slate-500',
|
||||
active && 'bg-slate-100',
|
||||
'block px-4 py-2 text-sm w-full text-left',
|
||||
),
|
||||
onClick,
|
||||
};
|
||||
|
||||
if (href == null) {
|
||||
return <button type="button" {...props} />;
|
||||
}
|
||||
|
||||
// TODO: Change to <Link> when there's a need for client-side navigation.
|
||||
return <a href={href} {...props} />;
|
||||
}}
|
||||
</Menu.Item>
|
||||
);
|
||||
}
|
62
packages/ui/src/Select/Select.tsx
Normal file
62
packages/ui/src/Select/Select.tsx
Normal file
@ -0,0 +1,62 @@
|
||||
import clsx from 'clsx';
|
||||
import { useId } from 'react';
|
||||
|
||||
export type SelectItem<T> = Readonly<{
|
||||
label: string;
|
||||
value: T;
|
||||
}>;
|
||||
|
||||
export type SelectDisplay = 'block' | 'inline';
|
||||
|
||||
type Props<T> = Readonly<{
|
||||
display?: SelectDisplay;
|
||||
isLabelHidden?: boolean;
|
||||
label: string;
|
||||
name?: string;
|
||||
onChange: (value: string) => void;
|
||||
options: ReadonlyArray<SelectItem<T>>;
|
||||
value: T;
|
||||
}>;
|
||||
|
||||
export default function Select<T>({
|
||||
display,
|
||||
label,
|
||||
isLabelHidden,
|
||||
name,
|
||||
options,
|
||||
value,
|
||||
onChange,
|
||||
}: Props<T>) {
|
||||
const id = useId();
|
||||
|
||||
return (
|
||||
<div>
|
||||
<label
|
||||
className={clsx(
|
||||
'mb-1 block text-sm font-medium text-slate-700',
|
||||
isLabelHidden && 'sr-only',
|
||||
)}
|
||||
htmlFor={id ?? undefined}>
|
||||
{label}
|
||||
</label>
|
||||
<select
|
||||
aria-label={isLabelHidden ? label : undefined}
|
||||
className={clsx(
|
||||
display === 'block' && 'block w-full',
|
||||
'focus:border-primary-500 focus:ring-primary-500 rounded-md border-slate-300 py-2 pl-3 pr-10 text-base focus:outline-none sm:text-sm',
|
||||
)}
|
||||
id={id}
|
||||
name={name ?? undefined}
|
||||
value={String(value)}
|
||||
onChange={(event) => {
|
||||
onChange(event.target.value);
|
||||
}}>
|
||||
{options.map(({ label: optionLabel, value: optionValue }) => (
|
||||
<option key={String(optionValue)} value={String(optionValue)}>
|
||||
{optionLabel}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
</div>
|
||||
);
|
||||
}
|
96
packages/ui/src/SlideOut/SlideOut.tsx
Normal file
96
packages/ui/src/SlideOut/SlideOut.tsx
Normal file
@ -0,0 +1,96 @@
|
||||
import clsx from 'clsx';
|
||||
import { Fragment } from 'react';
|
||||
import { Dialog, Transition } from '@headlessui/react';
|
||||
import { XMarkIcon } from '@heroicons/react/24/outline';
|
||||
|
||||
export type SlideOutSize = 'lg' | 'md' | 'sm' | 'xl';
|
||||
export type SlideOutEnterFrom = 'end' | 'start';
|
||||
|
||||
type Props = Readonly<{
|
||||
children: React.ReactNode;
|
||||
enterFrom?: SlideOutEnterFrom;
|
||||
isShown?: boolean;
|
||||
onClose?: () => void;
|
||||
size: SlideOutSize;
|
||||
title?: string;
|
||||
}>;
|
||||
|
||||
const sizeClasses: Record<SlideOutSize, string> = {
|
||||
lg: 'max-w-lg',
|
||||
md: 'max-w-md',
|
||||
sm: 'max-w-sm',
|
||||
xl: 'max-w-xl',
|
||||
};
|
||||
|
||||
const enterFromClasses: Record<
|
||||
SlideOutEnterFrom,
|
||||
Readonly<{ hidden: string; position: string; shown: string }>
|
||||
> = {
|
||||
end: {
|
||||
hidden: 'translate-x-full',
|
||||
position: 'ml-auto',
|
||||
shown: 'translate-x-0',
|
||||
},
|
||||
start: {
|
||||
hidden: '-translate-x-full',
|
||||
position: 'mr-auto',
|
||||
shown: 'translate-x-0',
|
||||
},
|
||||
};
|
||||
|
||||
export default function SlideOut({
|
||||
children,
|
||||
enterFrom = 'end',
|
||||
isShown = false,
|
||||
size,
|
||||
title,
|
||||
onClose,
|
||||
}: Props) {
|
||||
const enterFromClass = enterFromClasses[enterFrom];
|
||||
|
||||
return (
|
||||
<Transition.Root as={Fragment} show={isShown}>
|
||||
<Dialog as="div" className="relative z-40" onClose={() => onClose?.()}>
|
||||
<Transition.Child
|
||||
as={Fragment}
|
||||
enter="transition-opacity ease-linear duration-300"
|
||||
enterFrom="opacity-0"
|
||||
enterTo="opacity-100"
|
||||
leave="transition-opacity ease-linear duration-300"
|
||||
leaveFrom="opacity-100"
|
||||
leaveTo="opacity-0">
|
||||
<div className="fixed inset-0 bg-black bg-opacity-25" />
|
||||
</Transition.Child>
|
||||
<div className="fixed inset-0 z-40 flex">
|
||||
<Transition.Child
|
||||
as={Fragment}
|
||||
enter="transition ease-in-out duration-300 transform"
|
||||
enterFrom={enterFromClass.hidden}
|
||||
enterTo={enterFromClass.shown}
|
||||
leave="transition ease-in-out duration-300 transform"
|
||||
leaveFrom={enterFromClass.shown}
|
||||
leaveTo={enterFromClass.hidden}>
|
||||
<Dialog.Panel
|
||||
className={clsx(
|
||||
'relative flex h-full w-full max-w-lg flex-col overflow-y-auto bg-white py-4 pb-6 shadow-xl',
|
||||
enterFromClass.position,
|
||||
sizeClasses[size],
|
||||
)}>
|
||||
<div className="flex items-center justify-between px-4">
|
||||
<h2 className="text-lg font-medium text-slate-900">{title}</h2>
|
||||
<button
|
||||
className="focus:ring-primary-500 -mr-2 flex h-10 w-10 items-center justify-center rounded-full p-2 text-slate-400 hover:text-slate-500 focus:outline-none focus:ring-2 focus:ring-inset"
|
||||
type="button"
|
||||
onClick={() => onClose?.()}>
|
||||
<span className="sr-only">Close menu</span>
|
||||
<XMarkIcon aria-hidden="true" className="h-6 w-6" />
|
||||
</button>
|
||||
</div>
|
||||
{children}
|
||||
</Dialog.Panel>
|
||||
</Transition.Child>
|
||||
</div>
|
||||
</Dialog>
|
||||
</Transition.Root>
|
||||
);
|
||||
}
|
65
packages/ui/src/Tabs/Tabs.tsx
Normal file
65
packages/ui/src/Tabs/Tabs.tsx
Normal file
@ -0,0 +1,65 @@
|
||||
import clsx from 'clsx';
|
||||
import Link from 'next/link';
|
||||
import type { UrlObject } from 'url';
|
||||
|
||||
export type TabItem<T> = Readonly<{
|
||||
href?: UrlObject | string;
|
||||
label: string;
|
||||
value: T;
|
||||
}>;
|
||||
|
||||
type Props<T> = Readonly<{
|
||||
label: string;
|
||||
onChange?: (value: T) => void;
|
||||
tabs: ReadonlyArray<TabItem<T>>;
|
||||
value: T;
|
||||
}>;
|
||||
|
||||
export default function Tabs<T>({ label, tabs, value, onChange }: Props<T>) {
|
||||
return (
|
||||
<div className="w-full">
|
||||
<div role="tablist">
|
||||
<div className="border-b border-slate-200">
|
||||
<nav aria-label={label} className="-mb-px flex space-x-4">
|
||||
{tabs.map((tab) => {
|
||||
const isSelected = tab.value === value;
|
||||
const commonProps = {
|
||||
'aria-label': tab.label,
|
||||
'aria-selected': isSelected,
|
||||
children: tab.label,
|
||||
className: clsx(
|
||||
isSelected
|
||||
? 'border-primary-500 text-primary-600'
|
||||
: 'border-transparent text-slate-500 hover:text-slate-700 hover:border-slate-300',
|
||||
'whitespace-nowrap py-4 px-1 border-b-2 font-medium text-sm',
|
||||
),
|
||||
onClick:
|
||||
onChange != null ? () => onChange(tab.value) : undefined,
|
||||
role: 'tab',
|
||||
};
|
||||
|
||||
if (tab.href != null) {
|
||||
// TODO: Allow passing in of Link component.
|
||||
return (
|
||||
<Link
|
||||
key={String(tab.value)}
|
||||
href={tab.href}
|
||||
{...commonProps}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<button
|
||||
key={String(tab.value)}
|
||||
type="button"
|
||||
{...commonProps}
|
||||
/>
|
||||
);
|
||||
})}
|
||||
</nav>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
109
packages/ui/src/TextInput/TextInput.tsx
Normal file
109
packages/ui/src/TextInput/TextInput.tsx
Normal file
@ -0,0 +1,109 @@
|
||||
import clsx from 'clsx';
|
||||
import type { ChangeEvent } from 'react';
|
||||
import React, { useId } from 'react';
|
||||
|
||||
type Props = Readonly<{
|
||||
autoComplete?: string;
|
||||
defaultValue?: string;
|
||||
endIcon?: React.ComponentType<React.ComponentProps<'svg'>>;
|
||||
errorMessage?: React.ReactNode;
|
||||
id?: string;
|
||||
isDisabled?: boolean;
|
||||
isLabelHidden?: boolean;
|
||||
label: string;
|
||||
name?: string;
|
||||
onChange?: (value: string, event: ChangeEvent<HTMLInputElement>) => void;
|
||||
placeholder?: string;
|
||||
startIcon?: React.ComponentType<React.ComponentProps<'svg'>>;
|
||||
type?: 'email' | 'password' | 'text';
|
||||
value?: string;
|
||||
}>;
|
||||
|
||||
type State = 'error' | 'normal';
|
||||
|
||||
const stateClasses: Record<State, string> = {
|
||||
error:
|
||||
'border-danger-300 text-danger-900 placeholder-danger-300 focus:outline-none focus:ring-danger-500 focus:border-danger-500',
|
||||
normal:
|
||||
'placeholder:text-slate-400 focus:ring-primary-500 focus:border-primary-500 border-slate-300',
|
||||
};
|
||||
|
||||
export default function TextInput({
|
||||
autoComplete,
|
||||
defaultValue,
|
||||
endIcon: EndIcon,
|
||||
errorMessage,
|
||||
id: idParam,
|
||||
isDisabled,
|
||||
isLabelHidden = false,
|
||||
label,
|
||||
name,
|
||||
placeholder,
|
||||
startIcon: StartIcon,
|
||||
type = 'text',
|
||||
value,
|
||||
onChange,
|
||||
}: Props) {
|
||||
const hasError = errorMessage != null;
|
||||
const generatedId = useId();
|
||||
const id = idParam ?? generatedId;
|
||||
const errorId = useId();
|
||||
const state: State = hasError ? 'error' : 'normal';
|
||||
|
||||
return (
|
||||
<div>
|
||||
<label
|
||||
className={clsx(
|
||||
isLabelHidden
|
||||
? 'sr-only'
|
||||
: 'block text-sm font-medium text-slate-700',
|
||||
)}
|
||||
htmlFor={id}>
|
||||
{label}
|
||||
</label>
|
||||
<div className="relative mt-1">
|
||||
{StartIcon && (
|
||||
<div className="pointer-events-none absolute inset-y-0 left-0 flex items-center pl-3">
|
||||
<StartIcon aria-hidden="true" className="h-5 w-5 text-slate-400" />
|
||||
</div>
|
||||
)}
|
||||
<input
|
||||
aria-describedby={hasError ? errorId : undefined}
|
||||
aria-invalid={hasError ? true : undefined}
|
||||
autoComplete={autoComplete}
|
||||
className={clsx(
|
||||
'block w-full rounded-md sm:text-sm',
|
||||
StartIcon && 'pl-10',
|
||||
EndIcon && 'pr-10',
|
||||
stateClasses[state],
|
||||
isDisabled && 'bg-slate-100',
|
||||
)}
|
||||
defaultValue={defaultValue}
|
||||
disabled={isDisabled}
|
||||
id={id}
|
||||
name={name}
|
||||
placeholder={placeholder}
|
||||
type={type}
|
||||
value={value != null ? value : undefined}
|
||||
onChange={(event) => {
|
||||
if (!onChange) {
|
||||
return;
|
||||
}
|
||||
|
||||
onChange(event.target.value, event);
|
||||
}}
|
||||
/>
|
||||
{EndIcon && (
|
||||
<div className="pointer-events-none absolute inset-y-0 right-0 flex items-center pr-3">
|
||||
<EndIcon aria-hidden="true" className="h-5 w-5 text-slate-400" />
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
{errorMessage && (
|
||||
<p className="text-danger-600 mt-2 text-sm" id={errorId}>
|
||||
{errorMessage}
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
@ -1,6 +1,30 @@
|
||||
// Alert
|
||||
export * from './Alert/Alert';
|
||||
export { default as Alert } from './Alert/Alert';
|
||||
// Badge
|
||||
export * from './Badge/Badge';
|
||||
export { default as Badge } from './Badge/Badge';
|
||||
// Button
|
||||
export * from './Button/Button';
|
||||
export { default as Button } from './Button/Button';
|
||||
// Dialog
|
||||
export * from './Dialog/Dialog';
|
||||
export { default as Dialog } from './Dialog/Dialog';
|
||||
// DropdownMenu
|
||||
export * from './DropdownMenu/DropdownMenu';
|
||||
export { default as DropdownMenu } from './DropdownMenu/DropdownMenu';
|
||||
// Select
|
||||
export * from './Select/Select';
|
||||
export { default as Select } from './Select/Select';
|
||||
// SlideOut
|
||||
export * from './SlideOut/SlideOut';
|
||||
export { default as SlideOut } from './SlideOut/SlideOut';
|
||||
// Spinner
|
||||
export * from './Spinner/Spinner';
|
||||
export { default as Spinner } from './Spinner/Spinner';
|
||||
// Tabs
|
||||
export * from './Tabs/Tabs';
|
||||
export { default as Tabs } from './Tabs/Tabs';
|
||||
// TextInput
|
||||
export * from './TextInput/TextInput';
|
||||
export { default as TextInput } from './TextInput/TextInput';
|
||||
|
Reference in New Issue
Block a user