Initial commit
This commit is contained in:
commit
fa30c04815
117 changed files with 33513 additions and 0 deletions
134
src/components/Header/Header.tsx
Normal file
134
src/components/Header/Header.tsx
Normal file
|
|
@ -0,0 +1,134 @@
|
|||
import React, { Fragment } from 'react';
|
||||
import { Disclosure, Menu, Transition } from '@headlessui/react';
|
||||
import { MenuIcon, XIcon } from '@heroicons/react/outline';
|
||||
import { Link, RouteComponentProps } from '@reach/router';
|
||||
import { useAuth } from 'src/services/auth';
|
||||
|
||||
const navigation = [
|
||||
{ name: 'Dashboard', to: '/dashboard' },
|
||||
{ name: 'Apps', to: '/apps' },
|
||||
{ name: 'Users', to: '/users' },
|
||||
];
|
||||
|
||||
function classNames(...classes: any[]) {
|
||||
return classes.filter(Boolean).join(' ');
|
||||
}
|
||||
|
||||
type HeaderProps = RouteComponentProps;
|
||||
|
||||
const Header: React.FC<HeaderProps> = () => {
|
||||
const { logOut } = useAuth();
|
||||
|
||||
return (
|
||||
<Disclosure as="nav" className="bg-white shadow">
|
||||
{({ open }) => (
|
||||
<>
|
||||
<div className="max-w-7xl mx-auto px-2 sm:px-6 lg:px-8">
|
||||
<div className="relative flex justify-between h-16">
|
||||
<div className="absolute inset-y-0 left-0 flex items-center sm:hidden">
|
||||
{/* Mobile menu button */}
|
||||
<Disclosure.Button className="inline-flex items-center justify-center p-2 rounded-md text-gray-400 hover:text-gray-500 hover:bg-gray-100 focus:outline-none focus:ring-2 focus:ring-inset focus:ring-primary-500">
|
||||
<span className="sr-only">Open main menu</span>
|
||||
{open ? (
|
||||
<XIcon className="block h-6 w-6" aria-hidden="true" />
|
||||
) : (
|
||||
<MenuIcon className="block h-6 w-6" aria-hidden="true" />
|
||||
)}
|
||||
</Disclosure.Button>
|
||||
</div>
|
||||
<div className="flex-1 flex items-center justify-center sm:items-stretch sm:justify-start">
|
||||
<div className="flex-shrink-0 flex items-center">
|
||||
<img className="block lg:hidden" src="/assets/logo-small.svg" alt="Stackspin" />
|
||||
<img className="hidden lg:block" src="/assets/logo.svg" alt="Stackspin" />
|
||||
</div>
|
||||
<div className="hidden sm:ml-6 sm:flex sm:space-x-8">
|
||||
{/* Current: "border-primary-500 text-gray-900", Default: "border-transparent text-gray-500 hover:border-gray-300 hover:text-gray-700" */}
|
||||
{navigation.map((item) => (
|
||||
<Link
|
||||
key={item.name}
|
||||
to={item.to}
|
||||
getProps={({ isCurrent }) => {
|
||||
// the object returned here is passed to the
|
||||
// anchor element's props
|
||||
return {
|
||||
className: isCurrent
|
||||
? 'border-primary-400 text-gray-900 inline-flex items-center px-1 pt-1 border-b-2 text-sm font-medium'
|
||||
: 'border-transparent text-gray-500 hover:border-gray-300 hover:text-gray-700 inline-flex items-center px-1 pt-1 border-b-2 text-sm font-medium',
|
||||
'aria-current': isCurrent ? 'page' : undefined,
|
||||
};
|
||||
}}
|
||||
>
|
||||
{item.name}
|
||||
</Link>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
<div className="absolute inset-y-0 right-0 flex items-center pr-2 sm:static sm:inset-auto sm:ml-6 sm:pr-0">
|
||||
{/* Profile dropdown */}
|
||||
<Menu as="div" className="ml-3 relative">
|
||||
<div>
|
||||
<Menu.Button className="bg-white rounded-full flex text-sm focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-primary">
|
||||
<span className="sr-only">Open user menu</span>
|
||||
<img
|
||||
className="h-8 w-8 rounded-full"
|
||||
src="https://images.unsplash.com/photo-1472099645785-5658abf4ff4e?ixlib=rb-1.2.1&ixid=eyJhcHBfaWQiOjEyMDd9&auto=format&fit=facearea&facepad=2&w=256&h=256&q=80"
|
||||
alt=""
|
||||
/>
|
||||
</Menu.Button>
|
||||
</div>
|
||||
<Transition
|
||||
as={Fragment}
|
||||
enter="transition ease-out duration-200"
|
||||
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="origin-top-right absolute right-0 mt-2 w-48 rounded-md shadow-lg py-1 bg-white ring-1 ring-black ring-opacity-5 focus:outline-none">
|
||||
<Menu.Item>
|
||||
{({ active }) => (
|
||||
<a
|
||||
onClick={() => logOut()}
|
||||
className={classNames(active ? 'bg-gray-100' : '', 'block px-4 py-2 text-sm text-gray-700')}
|
||||
>
|
||||
Sign out
|
||||
</a>
|
||||
)}
|
||||
</Menu.Item>
|
||||
</Menu.Items>
|
||||
</Transition>
|
||||
</Menu>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<Disclosure.Panel className="sm:hidden">
|
||||
<div className="pt-2 pb-4 space-y-1">
|
||||
{navigation.map((item) => (
|
||||
<Link
|
||||
key={item.name}
|
||||
to={item.to}
|
||||
getProps={({ isCurrent }) => {
|
||||
// the object returned here is passed to the
|
||||
// anchor element's props
|
||||
return {
|
||||
className: isCurrent
|
||||
? 'bg-primary-50 border-primary-400 text-primary-700 block pl-3 pr-4 py-2 border-l-4 text-base font-medium'
|
||||
: 'border-transparent text-gray-500 hover:bg-gray-50 hover:border-gray-300 hover:text-gray-700 block pl-3 pr-4 py-2 border-l-4 text-base font-medium',
|
||||
'aria-current': isCurrent ? 'page' : undefined,
|
||||
};
|
||||
}}
|
||||
>
|
||||
{item.name}
|
||||
</Link>
|
||||
))}
|
||||
</div>
|
||||
</Disclosure.Panel>
|
||||
</>
|
||||
)}
|
||||
</Disclosure>
|
||||
);
|
||||
};
|
||||
|
||||
export default Header;
|
||||
1
src/components/Header/index.ts
Normal file
1
src/components/Header/index.ts
Normal file
|
|
@ -0,0 +1 @@
|
|||
export { default as Header } from './Header';
|
||||
17
src/components/Layout/Layout.tsx
Normal file
17
src/components/Layout/Layout.tsx
Normal file
|
|
@ -0,0 +1,17 @@
|
|||
import React from 'react';
|
||||
import { RouteComponentProps } from '@reach/router';
|
||||
import { Header } from '../Header';
|
||||
|
||||
type DashboardProps = RouteComponentProps;
|
||||
|
||||
const Layout: React.FC<DashboardProps> = ({ children }) => {
|
||||
return (
|
||||
<>
|
||||
<Header />
|
||||
|
||||
{children}
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default Layout;
|
||||
1
src/components/Layout/index.ts
Normal file
1
src/components/Layout/index.ts
Normal file
|
|
@ -0,0 +1 @@
|
|||
export { default as Layout } from './Layout';
|
||||
89
src/components/Modal/ConfirmationModal/ConfirmationModal.tsx
Normal file
89
src/components/Modal/ConfirmationModal/ConfirmationModal.tsx
Normal file
|
|
@ -0,0 +1,89 @@
|
|||
import React, { Fragment, useRef } from 'react';
|
||||
import { Dialog, Transition } from '@headlessui/react';
|
||||
import { ExclamationIcon } from '@heroicons/react/outline';
|
||||
|
||||
type ConfirmationModalProps = {
|
||||
open: boolean;
|
||||
onClose: () => void;
|
||||
title: string;
|
||||
body: string;
|
||||
};
|
||||
|
||||
export const ConfirmationModal = ({ open, onClose, title, body }: ConfirmationModalProps) => {
|
||||
const cancelButtonRef = useRef(null);
|
||||
|
||||
return (
|
||||
<Transition.Root show={open} as={Fragment}>
|
||||
<Dialog
|
||||
as="div"
|
||||
auto-reopen="true"
|
||||
className="fixed z-10 inset-0 overflow-y-auto"
|
||||
initialFocus={cancelButtonRef}
|
||||
onClose={onClose}
|
||||
>
|
||||
<div className="flex items-end justify-center min-h-screen pt-4 px-4 pb-20 text-center sm:block sm:p-0">
|
||||
<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"
|
||||
>
|
||||
<Dialog.Overlay className="fixed inset-0 bg-gray-500 bg-opacity-75 transition-opacity" />
|
||||
</Transition.Child>
|
||||
|
||||
{/* This element is to trick the browser into centering the modal contents. */}
|
||||
<span className="hidden sm:inline-block sm:align-middle sm:h-screen" aria-hidden="true">
|
||||
​
|
||||
</span>
|
||||
<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"
|
||||
>
|
||||
<div className="inline-block align-bottom bg-white rounded-lg text-left overflow-hidden shadow-xl transform transition-all sm:my-8 sm:align-middle sm:max-w-lg sm:w-full">
|
||||
<div className="bg-white px-4 pt-5 pb-4 sm:p-6 sm:pb-4">
|
||||
<div className="sm:flex sm:items-start">
|
||||
<div className="mx-auto flex-shrink-0 flex items-center justify-center h-12 w-12 rounded-full bg-red-100 sm:mx-0 sm:h-10 sm:w-10">
|
||||
<ExclamationIcon className="h-6 w-6 text-red-600" aria-hidden="true" />
|
||||
</div>
|
||||
<div className="mt-3 text-center sm:mt-0 sm:ml-4 sm:text-left">
|
||||
<Dialog.Title as="h3" className="text-lg leading-6 font-medium text-gray-900">
|
||||
{title}
|
||||
</Dialog.Title>
|
||||
<div className="mt-2">
|
||||
<p className="text-sm text-gray-500">{body}</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="bg-gray-50 px-4 py-3 sm:px-6 sm:flex sm:flex-row-reverse">
|
||||
<button
|
||||
type="button"
|
||||
className="w-full inline-flex justify-center rounded-md border border-transparent shadow-sm px-4 py-2 bg-red-600 text-base font-medium text-white hover:bg-red-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-red-500 sm:ml-3 sm:w-auto sm:text-sm"
|
||||
onClick={onClose}
|
||||
>
|
||||
Delete
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
className="mt-3 w-full inline-flex justify-center rounded-md border border-gray-300 shadow-sm px-4 py-2 bg-white text-base font-medium text-gray-700 hover:bg-gray-50 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-primary sm:mt-0 sm:ml-3 sm:w-auto sm:text-sm"
|
||||
onClick={onClose}
|
||||
ref={cancelButtonRef}
|
||||
>
|
||||
Cancel
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</Transition.Child>
|
||||
</div>
|
||||
</Dialog>
|
||||
</Transition.Root>
|
||||
);
|
||||
};
|
||||
1
src/components/Modal/ConfirmationModal/index.ts
Normal file
1
src/components/Modal/ConfirmationModal/index.ts
Normal file
|
|
@ -0,0 +1 @@
|
|||
export { ConfirmationModal } from './ConfirmationModal';
|
||||
1
src/components/Modal/index.ts
Normal file
1
src/components/Modal/index.ts
Normal file
|
|
@ -0,0 +1 @@
|
|||
export { ConfirmationModal } from './ConfirmationModal';
|
||||
178
src/components/Table/Table.tsx
Normal file
178
src/components/Table/Table.tsx
Normal file
|
|
@ -0,0 +1,178 @@
|
|||
import { ArrowSmDownIcon, ArrowSmUpIcon } from '@heroicons/react/solid';
|
||||
import React, { useEffect } from 'react';
|
||||
import { useTable, useRowSelect, Column, IdType, useSortBy } from 'react-table';
|
||||
|
||||
export interface ReactTableProps<T extends Record<string, unknown>> {
|
||||
columns: Column<T>[];
|
||||
data: T[];
|
||||
onRowClick?(row: T): void;
|
||||
pagination?: boolean;
|
||||
getSelectedRowIds?(rows: Record<IdType<T>, boolean>): void;
|
||||
selectable?: boolean;
|
||||
}
|
||||
|
||||
const IndeterminateCheckbox = React.forwardRef(({ indeterminate, ...rest }: any, ref) => {
|
||||
const defaultRef = React.useRef(null);
|
||||
const resolvedRef: any = ref || defaultRef;
|
||||
|
||||
React.useEffect(() => {
|
||||
resolvedRef.current.indeterminate = indeterminate;
|
||||
}, [resolvedRef, indeterminate]);
|
||||
|
||||
return (
|
||||
<>
|
||||
<input
|
||||
type="checkbox"
|
||||
ref={resolvedRef}
|
||||
{...rest}
|
||||
className="focus:ring-primary-800 h-4 w-4 text-primary-700 border-gray-300 rounded"
|
||||
/>
|
||||
</>
|
||||
);
|
||||
});
|
||||
|
||||
export const Table = <T extends Record<string, unknown>>({
|
||||
columns,
|
||||
data,
|
||||
pagination = false,
|
||||
onRowClick,
|
||||
getSelectedRowIds,
|
||||
selectable = false,
|
||||
}: ReactTableProps<T>) => {
|
||||
const {
|
||||
getTableProps,
|
||||
getTableBodyProps,
|
||||
headerGroups,
|
||||
rows,
|
||||
prepareRow,
|
||||
pageCount,
|
||||
state: { selectedRowIds },
|
||||
} = useTable(
|
||||
{
|
||||
columns,
|
||||
data,
|
||||
},
|
||||
useSortBy,
|
||||
useRowSelect,
|
||||
selectable
|
||||
? (hooks) => {
|
||||
hooks.visibleColumns.push((columns2) => [
|
||||
{
|
||||
id: 'selection',
|
||||
Header: ({ getToggleAllRowsSelectedProps }: { getToggleAllRowsSelectedProps: any }) => (
|
||||
<div>
|
||||
<IndeterminateCheckbox {...getToggleAllRowsSelectedProps()} />
|
||||
</div>
|
||||
),
|
||||
Cell: ({ row }: { row: any }) => (
|
||||
<div>
|
||||
<IndeterminateCheckbox {...row.getToggleRowSelectedProps()} />
|
||||
</div>
|
||||
),
|
||||
width: 16,
|
||||
},
|
||||
...columns2,
|
||||
]);
|
||||
}
|
||||
: () => {},
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
if (selectedRowIds && getSelectedRowIds) {
|
||||
getSelectedRowIds(selectedRowIds);
|
||||
}
|
||||
}, [selectedRowIds, getSelectedRowIds]);
|
||||
|
||||
return (
|
||||
<>
|
||||
<table className="min-w-full divide-y divide-gray-200 table-auto" {...getTableProps()}>
|
||||
<thead className="bg-gray-50">
|
||||
{headerGroups.map((headerGroup: any, index: any) => (
|
||||
<tr {...headerGroup.getHeaderGroupProps()} key={index!}>
|
||||
{headerGroup.headers.map((column: any) => (
|
||||
<th
|
||||
key={column}
|
||||
{...column.getHeaderProps([
|
||||
{
|
||||
style: {
|
||||
width: column.width ? column.width : 'auto !important',
|
||||
},
|
||||
},
|
||||
column.getSortByToggleProps(),
|
||||
])}
|
||||
scope="col"
|
||||
className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider"
|
||||
>
|
||||
<div className="flex items-center">
|
||||
<span>{column.render('Header')}</span>
|
||||
{(column as any).isSorted ? (
|
||||
(column as any).isSortedDesc ? (
|
||||
<ArrowSmDownIcon className="w-4 h-4 text-gray-400 ml-1" />
|
||||
) : (
|
||||
<ArrowSmUpIcon className="w-4 h-4 text-gray-400 ml-1" />
|
||||
)
|
||||
) : (
|
||||
''
|
||||
)}
|
||||
</div>
|
||||
</th>
|
||||
))}
|
||||
</tr>
|
||||
))}
|
||||
</thead>
|
||||
<tbody {...getTableBodyProps()}>
|
||||
{rows.map((row: any, rowIndex) => {
|
||||
prepareRow(row);
|
||||
return (
|
||||
<tr
|
||||
key={row}
|
||||
{...row.getRowProps()}
|
||||
className={rowIndex % 2 === 0 ? 'bg-white group' : 'bg-gray-50 group'}
|
||||
onClick={onRowClick ? () => onRowClick(row.original as T) : () => {}}
|
||||
>
|
||||
{row.cells.map((cell: any) => {
|
||||
return (
|
||||
<td
|
||||
key={cell}
|
||||
{...cell.getCellProps()}
|
||||
className="px-6 py-4 whitespace-nowrap text-sm text-gray-500"
|
||||
>
|
||||
{cell.render('Cell')}
|
||||
</td>
|
||||
);
|
||||
})}
|
||||
</tr>
|
||||
);
|
||||
})}
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
{pagination && pageCount > 1 && (
|
||||
<div className="bg-white px-4 py-3 flex items-center justify-between border-t border-gray-100 sm:px-6">
|
||||
<div className="flex-1 flex justify-between sm:hidden">
|
||||
<a
|
||||
href="#"
|
||||
className="relative inline-flex items-center px-4 py-2 border border-gray-300 text-sm font-medium rounded-md text-gray-700 bg-white hover:bg-gray-50"
|
||||
>
|
||||
Previous
|
||||
</a>
|
||||
<a
|
||||
href="#"
|
||||
className="ml-3 relative inline-flex items-center px-4 py-2 border border-gray-300 text-sm font-medium rounded-md text-gray-700 bg-white hover:bg-gray-50"
|
||||
>
|
||||
Next
|
||||
</a>
|
||||
</div>
|
||||
<div className="hidden sm:flex-1 sm:flex sm:items-center sm:justify-between">
|
||||
<div>
|
||||
<p className="text-sm text-gray-700">
|
||||
Showing <span className="font-medium">1</span> to <span className="font-medium">3</span> of{' '}
|
||||
<span className="font-medium">3</span> results
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
};
|
||||
1
src/components/Table/index.ts
Normal file
1
src/components/Table/index.ts
Normal file
|
|
@ -0,0 +1 @@
|
|||
export { Table } from './Table';
|
||||
8
src/components/Tabs/TabPanel.tsx
Normal file
8
src/components/Tabs/TabPanel.tsx
Normal file
|
|
@ -0,0 +1,8 @@
|
|||
import React from 'react';
|
||||
import { TabPanelProps } from './types';
|
||||
|
||||
export const TabPanel = ({ children, isActive }: TabPanelProps) => {
|
||||
if (!isActive) return null;
|
||||
|
||||
return <div className="pt-8 pb-4">{children}</div>;
|
||||
};
|
||||
64
src/components/Tabs/Tabs.tsx
Normal file
64
src/components/Tabs/Tabs.tsx
Normal file
|
|
@ -0,0 +1,64 @@
|
|||
import React, { useState } from 'react';
|
||||
import { TabPanel } from './TabPanel';
|
||||
import { TabsProps } from './types';
|
||||
|
||||
export const Tabs = ({ tabs }: TabsProps) => {
|
||||
const [activeTabIndex, setActiveTabIndex] = useState<number>(0);
|
||||
|
||||
const handleTabPress = (index: number) => () => {
|
||||
setActiveTabIndex(index);
|
||||
};
|
||||
|
||||
function classNames(...classes: any) {
|
||||
return classes.filter(Boolean).join(' ');
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className="sm:hidden">
|
||||
<label htmlFor="tabs" className="sr-only">
|
||||
Select a tab
|
||||
</label>
|
||||
<select
|
||||
id="tabs"
|
||||
name="tabs"
|
||||
className="block w-full focus:ring-primary-500 focus:border-primary-500 border-gray-300 rounded-md"
|
||||
// defaultValue={tabs ? tabs.find((tab) => tab.current).name : undefined}
|
||||
>
|
||||
{tabs.map((tab) => (
|
||||
<option key={tab.name}>{tab.name}</option>
|
||||
))}
|
||||
</select>
|
||||
</div>
|
||||
<div className="hidden sm:block">
|
||||
<nav className="flex space-x-4" aria-label="Tabs">
|
||||
{tabs.map((tab, tabIndex) => (
|
||||
<a
|
||||
onClick={handleTabPress(tabIndex)}
|
||||
key={tab.name}
|
||||
className={classNames(
|
||||
activeTabIndex === tabIndex
|
||||
? 'bg-gray-100 text-gray-700'
|
||||
: 'text-gray-500 hover:text-gray-700 hover:bg-gray-50',
|
||||
'px-3 py-2 font-medium text-sm rounded-md cursor-pointer',
|
||||
)}
|
||||
aria-current={activeTabIndex === tabIndex ? 'page' : undefined}
|
||||
>
|
||||
{tab.name}
|
||||
</a>
|
||||
))}
|
||||
</nav>
|
||||
</div>
|
||||
|
||||
{tabs.map(({ component, name, disabled }, index) =>
|
||||
disabled ? (
|
||||
<React.Fragment key={name} />
|
||||
) : (
|
||||
<TabPanel key={name} isActive={activeTabIndex === index}>
|
||||
{component}
|
||||
</TabPanel>
|
||||
),
|
||||
)}
|
||||
</>
|
||||
);
|
||||
};
|
||||
1
src/components/Tabs/index.ts
Normal file
1
src/components/Tabs/index.ts
Normal file
|
|
@ -0,0 +1 @@
|
|||
export { Tabs } from './Tabs';
|
||||
21
src/components/Tabs/types.ts
Normal file
21
src/components/Tabs/types.ts
Normal file
|
|
@ -0,0 +1,21 @@
|
|||
type Tab = {
|
||||
disabled?: boolean;
|
||||
name: string;
|
||||
component: JSX.Element;
|
||||
};
|
||||
|
||||
export interface TabsProps {
|
||||
tabs: Tab[];
|
||||
}
|
||||
|
||||
export interface TabPanelProps {
|
||||
isActive: boolean;
|
||||
children: JSX.Element | JSX.Element[];
|
||||
}
|
||||
|
||||
export interface TabProps {
|
||||
title: string;
|
||||
isActive: boolean;
|
||||
onPress(): void;
|
||||
disabled: boolean;
|
||||
}
|
||||
4
src/components/index.ts
Normal file
4
src/components/index.ts
Normal file
|
|
@ -0,0 +1,4 @@
|
|||
export { Layout } from './Layout';
|
||||
export { Header } from './Header';
|
||||
export { Table } from './Table';
|
||||
export { Tabs } from './Tabs';
|
||||
Reference in a new issue