diff --git a/apps/admin-x-design-system/.storybook/preview.tsx b/apps/admin-x-design-system/.storybook/preview.tsx
index 6f1dfede99..f844e552ce 100644
--- a/apps/admin-x-design-system/.storybook/preview.tsx
+++ b/apps/admin-x-design-system/.storybook/preview.tsx
@@ -18,7 +18,7 @@ const preview: Preview = {
},
options: {
storySort: {
- mathod: 'alphabetical',
+ method: 'alphabetical',
order: ['Welcome', 'Foundations', ['Style Guide', 'Colors', 'Icons', 'ErrorHandling'], 'Global', ['Form', 'Chrome', 'Modal', 'Layout', 'List', 'Table', '*'], 'Settings', ['Setting Section', 'Setting Group', '*'], 'Experimental'],
},
},
@@ -32,7 +32,10 @@ const preview: Preview = {
return (
{/* 👇 Decorators in Storybook also accept a function. Replace
with Story() to enable it */}
diff --git a/apps/admin-x-design-system/src/Boilerplate.stories.tsx b/apps/admin-x-design-system/src/Boilerplate.stories.tsx
index af0ba5b8a3..523f76b978 100644
--- a/apps/admin-x-design-system/src/Boilerplate.stories.tsx
+++ b/apps/admin-x-design-system/src/Boilerplate.stories.tsx
@@ -3,7 +3,7 @@ import type {Meta, StoryObj} from '@storybook/react';
import BoilerPlate from './Boilerplate';
const meta = {
- title: 'Meta / Boilerplate story',
+ title: 'Meta / Boilerplate',
component: BoilerPlate,
tags: ['autodocs']
} satisfies Meta
;
diff --git a/apps/admin-x-design-system/src/assets/icons/cardview.svg b/apps/admin-x-design-system/src/assets/icons/cardview.svg
new file mode 100644
index 0000000000..88d8b62e05
--- /dev/null
+++ b/apps/admin-x-design-system/src/assets/icons/cardview.svg
@@ -0,0 +1 @@
+layout-module-1
\ No newline at end of file
diff --git a/apps/admin-x-design-system/src/assets/icons/listview.svg b/apps/admin-x-design-system/src/assets/icons/listview.svg
new file mode 100644
index 0000000000..7a6cfeed7e
--- /dev/null
+++ b/apps/admin-x-design-system/src/assets/icons/listview.svg
@@ -0,0 +1 @@
+layout-headline
\ No newline at end of file
diff --git a/apps/admin-x-design-system/src/global/Breadcrumbs.tsx b/apps/admin-x-design-system/src/global/Breadcrumbs.tsx
index aa93dfafae..a204043fad 100644
--- a/apps/admin-x-design-system/src/global/Breadcrumbs.tsx
+++ b/apps/admin-x-design-system/src/global/Breadcrumbs.tsx
@@ -10,6 +10,7 @@ export type BreadcrumbItem = {
export interface BreadcrumbsProps {
items: BreadcrumbItem[];
backIcon?: boolean;
+ snapBackIcon?: boolean;
onBack?: () => void;
containerClassName?: string;
itemClassName?: string;
@@ -20,6 +21,7 @@ export interface BreadcrumbsProps {
const Breadcrumbs: React.FC = ({
items,
backIcon = false,
+ snapBackIcon = true,
onBack,
containerClassName,
itemClassName,
@@ -47,7 +49,7 @@ const Breadcrumbs: React.FC = ({
return (
{backIcon &&
-
+
}
{items.map((item) => {
const bcItem = (i === allItems - 1 ?
diff --git a/apps/admin-x-design-system/src/global/Button.tsx b/apps/admin-x-design-system/src/global/Button.tsx
index dbf426f838..37230881e5 100644
--- a/apps/admin-x-design-system/src/global/Button.tsx
+++ b/apps/admin-x-design-system/src/global/Button.tsx
@@ -54,7 +54,8 @@ const Button: React.FC
= ({
className = clsx(
'inline-flex items-center justify-center whitespace-nowrap rounded text-sm transition',
((link && color !== 'clear' && color !== 'black') || (!link && color !== 'clear')) ? 'font-bold' : 'font-semibold',
- !link ? `${size === 'sm' ? ' h-7 px-3 ' : ' h-[34px] px-4 '}` : '',
+ !link ? `${size === 'sm' ? 'h-7' : 'h-[34px]'}` : '',
+ !link ? `${size === 'sm' || label && icon ? 'px-3' : 'px-4'}` : '',
(link && linkWithPadding) && '-m-1 p-1',
className
);
@@ -125,7 +126,7 @@ const Button: React.FC = ({
labelClasses += loading ? 'invisible' : '';
const buttonChildren = <>
- {icon && }
+ {icon && }
{label}
{loading && Loading...
}
>;
diff --git a/apps/admin-x-design-system/src/global/Heading.tsx b/apps/admin-x-design-system/src/global/Heading.tsx
index 427213cea0..69603e2cd9 100644
--- a/apps/admin-x-design-system/src/global/Heading.tsx
+++ b/apps/admin-x-design-system/src/global/Heading.tsx
@@ -62,7 +62,7 @@ const Heading: React.FC = ({
if (!useLabelTag) {
switch (level) {
case 1:
- styles += ' md:text-5xl leading-tighter';
+ styles += ' md:text-4xl leading-tighter';
break;
case 2:
styles += ' md:text-3xl';
diff --git a/apps/admin-x-design-system/src/global/Pagination.stories.tsx b/apps/admin-x-design-system/src/global/Pagination.stories.tsx
index a58a7fc18e..99944139a7 100644
--- a/apps/admin-x-design-system/src/global/Pagination.stories.tsx
+++ b/apps/admin-x-design-system/src/global/Pagination.stories.tsx
@@ -3,7 +3,7 @@ import type {Meta, StoryObj} from '@storybook/react';
import Pagination from './Pagination';
const meta = {
- title: 'Global / Pagination story',
+ title: 'Global / Pagination',
component: Pagination,
tags: ['autodocs']
} satisfies Meta;
diff --git a/apps/admin-x-design-system/src/global/TabView.tsx b/apps/admin-x-design-system/src/global/TabView.tsx
index ca8fd8361e..49b9448e91 100644
--- a/apps/admin-x-design-system/src/global/TabView.tsx
+++ b/apps/admin-x-design-system/src/global/TabView.tsx
@@ -12,12 +12,96 @@ export type Tab = {
contents?: React.ReactNode;
}
+export type TabWidth = 'narrow' | 'normal' | 'wide';
+
+export interface TabButtonProps {
+ id: ID,
+ title: string;
+ onClick?: (e:React.MouseEvent) => void;
+ selected: boolean;
+ border?: boolean;
+ counter?: number | null;
+}
+
+export const TabButton: React.FC = ({
+ id,
+ title,
+ onClick,
+ selected,
+ border,
+ counter
+}) => {
+ return (
+
+ {title}
+ {(typeof counter === 'number') && {counter} }
+
+ );
+};
+
+export interface TabListProps {
+ tabs: readonly Tab[];
+ width: TabWidth;
+ handleTabChange?: (e: React.MouseEvent) => void;
+ border: boolean;
+ buttonBorder?: boolean;
+ selectedTab?: ID
+}
+
+export const TabList: React.FC = ({
+ tabs,
+ width = 'normal',
+ handleTabChange,
+ border,
+ buttonBorder,
+ selectedTab
+}) => {
+ const containerClasses = clsx(
+ 'no-scrollbar flex w-full overflow-x-auto',
+ width === 'narrow' && 'gap-3',
+ width === 'normal' && 'gap-5',
+ width === 'wide' && 'gap-7',
+ border && 'border-b border-grey-300 dark:border-grey-900'
+ );
+ return (
+
+ {tabs.map(tab => (
+
+
+
+ ))}
+
+ );
+};
+
export interface TabViewProps {
tabs: readonly Tab[];
onTabChange: (id: ID) => void;
selectedTab?: ID;
border?: boolean;
- width?: 'narrow' | 'normal' | 'wide';
+ buttonBorder?: boolean;
+ width?: TabWidth;
}
function TabView({
@@ -25,6 +109,7 @@ function TabView({
onTabChange,
selectedTab,
border = true,
+ buttonBorder = border,
width = 'normal'
}: TabViewProps) {
if (tabs.length !== 0 && selectedTab === undefined) {
@@ -40,40 +125,16 @@ function TabView({
onTabChange(newTab);
};
- const containerClasses = clsx(
- 'no-scrollbar flex w-full overflow-x-auto',
- width === 'narrow' && 'gap-3',
- width === 'normal' && 'gap-5',
- width === 'wide' && 'gap-7',
- border && 'border-b border-grey-300 dark:border-grey-900'
- );
-
return (
-
- {tabs.map(tab => (
-
-
- {tab.title}
- {(typeof tab.counter === 'number') && {tab.counter} }
-
-
- ))}
-
+
{tabs.map((tab) => {
return (
<>
diff --git a/apps/admin-x-design-system/src/global/Table.stories.tsx b/apps/admin-x-design-system/src/global/Table.stories.tsx
index 4bd3565080..bd294e5e6b 100644
--- a/apps/admin-x-design-system/src/global/Table.stories.tsx
+++ b/apps/admin-x-design-system/src/global/Table.stories.tsx
@@ -152,3 +152,95 @@ const SortableTable = () => {
export const Sortable: Story = {
render: () =>
};
+
+/**
+ * Sticky header
+ */
+
+// const complexTableHeader = (sticky: boolean) => (
+// <>
+// Member
+// Status
+// Open rate
+// Location
+// Created
+// Signed up on post
+// Newsletter
+// Billing Period
+// Email sent
+// >
+// );
+
+// const complexTableRows = (rows: number) => {
+// const data = [];
+// for (let i = 0; i < rows; i++) {
+// data.push(
+// <>
+//
+//
+//
+// {i % 3 === 0 &&
}
+// {i % 3 === 1 &&
}
+// {i % 3 === 2 &&
}
+//
+// {i % 3 === 0 &&
Jamie Larson
}
+// {i % 3 === 1 &&
Giana Septimus
}
+// {i % 3 === 2 &&
Zaire Bator
}
+//
jamie@larson.com
+//
+//
+//
+// Free
+// 40%
+// London, UK
+// 22 June 2023
+// Hiking in the Nordic
+// Subscribed
+// Monthly
+// 1,303
+//
+// >
+// );
+// }
+// return data;
+// };
+
+// export const HorizontalScroll: Story = {
+// args: {
+// header: complexTableHeader(false),
+// children: complexTableRows(100),
+// hint: 'Massive table',
+// hintSeparator: true
+// }
+// };
+
+// export const FillContainer: Story = {
+// args: {
+// fillContainer: true,
+// header: complexTableHeader(true),
+// children: complexTableRows(50),
+// hint: 'Massive table',
+// hintSeparator: true
+// }
+// };
+
+// export const PageExample: Story = {
+// decorators: [(_story: () => ReactNode) => (
+//
+//
+//
Page title
+//
This example shows how you can create a page with arbitrary content on the top and a large table at the bottom that fills up the remaining space. The table has a sticky header row, a footer that is always visible and scrolling vertically and horizontally (resize the window to see the effect).
+//
The size and positioning of the table is completely controlled by its container . The container must have `relative` position. Use a column flexbox as the main container of the page then set the table container to flex-auto to fill the available horizontal space.
+//
{_story()}
+//
+//
+// )],
+// args: {
+// fillContainer: true,
+// header: complexTableHeader(true),
+// children: complexTableRows(50),
+// hint: 'The footer of the table sticks to the bottom to stay visible',
+// hintSeparator: true,
+// paddingXClassName: 'px-10'
+// }
+// };
\ No newline at end of file
diff --git a/apps/admin-x-design-system/src/global/Table.tsx b/apps/admin-x-design-system/src/global/Table.tsx
index e7d21055b2..927b3e2d48 100644
--- a/apps/admin-x-design-system/src/global/Table.tsx
+++ b/apps/admin-x-design-system/src/global/Table.tsx
@@ -17,8 +17,8 @@ export interface TableProps {
/**
* If the table is the primary content on a page (e.g. Members table) then you can set a pagetitle to be consistent
*/
- pageTitle?: string;
header?: React.ReactNode;
+ pageTitle?: string;
children?: React.ReactNode;
borderTop?: boolean;
hint?: React.ReactNode;
@@ -27,6 +27,8 @@ export interface TableProps {
isLoading?: boolean;
pagination?: PaginationData;
showMore?: ShowMoreData;
+ fillContainer?: boolean;
+ paddingXClassName?: string;
}
const OptionalPagination = ({pagination}: {pagination?: PaginationData}) => {
@@ -51,14 +53,20 @@ const OptionalShowMore = ({showMore}: {showMore?: ShowMoreData}) => {
);
};
-const Table: React.FC = ({header, children, borderTop, hint, hintSeparator, pageTitle, className, pagination, showMore, isLoading}) => {
- const tableClasses = clsx(
- (borderTop || pageTitle) && 'border-t border-grey-300',
- 'w-full overflow-x-auto',
- pageTitle ? 'mb-0 mt-14' : 'my-0',
- className
- );
-
+const Table: React.FC = ({
+ header,
+ children,
+ borderTop,
+ hint,
+ hintSeparator,
+ pageTitle,
+ className,
+ pagination,
+ showMore,
+ isLoading,
+ fillContainer = false,
+ paddingXClassName
+}) => {
const table = React.useRef(null);
const maxTableHeight = React.useRef(0);
const [tableHeight, setTableHeight] = React.useState(undefined);
@@ -111,33 +119,75 @@ const Table: React.FC = ({header, children, borderTop, hint, hintSep
};
}, [tableHeight]);
+ const headerClasses = clsx(
+ 'h-9 border-b border-grey-200 dark:border-grey-600'
+ );
+
+ /**
+ * To have full-bleed scroll try this:
+ * - unset width of table
+ * - set minWidth of table to 100%
+ * - set side padding of table to 40px
+ * - unset tableContainer width
+ * - set minWidth of tableContainer to 100%
+ * - unset mainContainer width
+ * - set minWidth of mainContainer to 100%
+ * - set side margins of outer container to -40px
+ * - set footer side paddings to 40px
+ */
+
+ const tableClasses = clsx(
+ 'w-full',
+ fillContainer ? 'min-w-full' : 'w-full',
+ (borderTop || pageTitle) && 'border-t border-grey-300',
+ pageTitle ? 'mb-0 mt-14' : 'my-0',
+ className
+ );
+
+ const mainContainerClasses = clsx(
+ 'overflow-x-auto',
+ fillContainer ? 'absolute inset-0 min-w-full' : 'w-full'
+ );
+
+ const tableContainerClasses = clsx(
+ fillContainer ? 'max-h-[calc(100%-38px)] w-full overflow-y-auto' : 'w-full',
+ paddingXClassName
+ );
+
+ const footerClasses = clsx(
+ 'sticky bottom-0 -mt-px bg-white pb-3',
+ paddingXClassName
+ );
+
return (
<>
-
+
{pageTitle &&
{pageTitle} }
-
- {header &&
- {header}
- }
- {!isLoading &&
- {children}
- }
+
+
+ {header &&
+ {header}
+ }
+ {!isLoading &&
+ {children}
+ }
- {multiplePages &&
}
-
+ {multiplePages &&
}
+
+
- {isLoading &&
}
+ {isLoading &&
}
{(hint || pagination || showMore) &&
-
+
{(hintSeparator || pagination) && }
{hint ?? ' '}
- }
+ }
>
);
diff --git a/apps/admin-x-design-system/src/global/TableCell.tsx b/apps/admin-x-design-system/src/global/TableCell.tsx
index fe6e3bdcd2..3bb08a5c83 100644
--- a/apps/admin-x-design-system/src/global/TableCell.tsx
+++ b/apps/admin-x-design-system/src/global/TableCell.tsx
@@ -3,12 +3,25 @@ import React, {HTMLProps} from 'react';
export interface TableCellProps extends HTMLProps {
padding?: boolean;
+ align?: 'left' | 'center' | 'right';
+ valign?: 'top' | 'center' | 'bottom';
}
-const TableCell: React.FC = ({className, children, padding = true, ...props}) => {
+const TableCell: React.FC = ({
+ className,
+ children,
+ padding = true,
+ align = 'left',
+ valign = 'top',
+ ...props
+}) => {
const tableCellClasses = clsx(
padding ? '!py-3 !pl-0 !pr-6' : '',
- 'align-top',
+ (align === 'center' && 'text-center'),
+ (align === 'right' && 'text-right'),
+ (valign === 'top' && 'align-top'),
+ (valign === 'center' && 'align-center'),
+ (valign === 'bottom' && 'align-bottom'),
props.onClick && 'hover:cursor-pointer',
className
);
diff --git a/apps/admin-x-design-system/src/global/TableHead.tsx b/apps/admin-x-design-system/src/global/TableHead.tsx
index 248cb7b625..8ccdbbe35d 100644
--- a/apps/admin-x-design-system/src/global/TableHead.tsx
+++ b/apps/admin-x-design-system/src/global/TableHead.tsx
@@ -2,11 +2,20 @@ import clsx from 'clsx';
import React, {HTMLProps} from 'react';
import Heading from './Heading';
-export type TableHeadProps = HTMLProps
+export interface TableHeadProps extends HTMLProps {
+ sticky?: boolean;
+}
-const TableHead: React.FC = ({className, children, colSpan, ...props}) => {
+const TableHead: React.FC = ({
+ className,
+ children,
+ colSpan,
+ sticky = false,
+ ...props
+}) => {
const tableCellClasses = clsx(
'!py-2 !pl-0 !pr-6 text-left align-top',
+ sticky && 'sticky top-0 bg-white',
props.onClick && 'hover:cursor-pointer',
className
);
diff --git a/apps/admin-x-design-system/src/global/TableRow.tsx b/apps/admin-x-design-system/src/global/TableRow.tsx
index f13a1fd254..7e40a3f502 100644
--- a/apps/admin-x-design-system/src/global/TableRow.tsx
+++ b/apps/admin-x-design-system/src/global/TableRow.tsx
@@ -1,6 +1,8 @@
import clsx from 'clsx';
import React, {forwardRef} from 'react';
+export const tableRowHoverBgClasses = 'hover:bg-gradient-to-r hover:from-white hover:to-grey-50 dark:hover:from-black dark:hover:to-grey-950';
+
export interface TableRowProps {
id?: string;
action?: React.ReactNode;
@@ -27,7 +29,7 @@ const TableRow = forwardRef(function TableRo
separator = (separator === undefined) ? true : separator;
const tableRowClasses = clsx(
'group/table-row',
- bgOnHover && 'hover:bg-gradient-to-r hover:from-white hover:to-grey-50 dark:hover:from-black dark:hover:to-grey-950',
+ bgOnHover && tableRowHoverBgClasses,
onClick && 'cursor-pointer',
separator ? 'border-b border-grey-100 last-of-type:border-b-transparent hover:border-grey-200 dark:border-grey-950 dark:hover:border-grey-900' : 'border-y border-none first-of-type:hover:border-t-transparent',
className
diff --git a/apps/admin-x-design-system/src/global/layout/GlobalActions.tsx b/apps/admin-x-design-system/src/global/layout/GlobalActions.tsx
new file mode 100644
index 0000000000..787a497b3e
--- /dev/null
+++ b/apps/admin-x-design-system/src/global/layout/GlobalActions.tsx
@@ -0,0 +1,10 @@
+import React from 'react';
+import Button from '../Button';
+
+const GlobalActions: React.FC = () => {
+ return (
+ {}} />
+ );
+};
+
+export default GlobalActions;
\ No newline at end of file
diff --git a/apps/admin-x-design-system/src/global/layout/Page.stories.tsx b/apps/admin-x-design-system/src/global/layout/Page.stories.tsx
new file mode 100644
index 0000000000..455d50a4ad
--- /dev/null
+++ b/apps/admin-x-design-system/src/global/layout/Page.stories.tsx
@@ -0,0 +1,449 @@
+import type {Meta, StoryObj} from '@storybook/react';
+import {useArgs} from '@storybook/preview-api';
+
+import Page, {CustomGlobalAction} from './Page';
+import {Tab} from '../TabView';
+import ViewContainer from './ViewContainer';
+
+import {testColumns, testRows} from '../table/DynamicTable.stories';
+import {exampleActions as exampleActionButtons} from './ViewContainer.stories';
+import DynamicTable from '../table/DynamicTable';
+import Hint from '../Hint';
+import Heading from '../Heading';
+import {tableRowHoverBgClasses} from '../TableRow';
+import Breadcrumbs from '../Breadcrumbs';
+import Avatar from '../Avatar';
+import Button from '../Button';
+import {Toggle} from '../..';
+
+const meta = {
+ title: 'Global / Layout / Page',
+ component: Page,
+ tags: ['autodocs'],
+ render: function Component(args) {
+ const [, updateArgs] = useArgs();
+
+ return {
+ updateArgs({selectedTab: tab});
+ args.onTabChange?.(tab);
+ }}
+ />;
+ }
+} satisfies Meta;
+
+export default meta;
+type Story = StoryObj;
+
+const dummyContent = Placeholder content
;
+
+const customGlobalActions: CustomGlobalAction[] = [
+ {
+ iconName: 'heart',
+ onClick: () => {
+ alert('Clicked on custom action');
+ }
+ }
+];
+
+const pageTabs: Tab[] = [
+ {
+ id: 'active',
+ title: 'Active'
+ },
+ {
+ id: 'archive',
+ title: 'Archive'
+ }
+];
+
+export const Default: Story = {
+ parameters: {
+ layout: 'fullscreen'
+ },
+ args: {
+ pageTabs: pageTabs,
+ children: dummyContent
+ }
+};
+
+export const WithHamburger: Story = {
+ parameters: {
+ layout: 'fullscreen'
+ },
+ args: {
+ pageTabs: pageTabs,
+ showPageMenu: true,
+ children: dummyContent
+ }
+};
+
+export const WithGlobalActions: Story = {
+ parameters: {
+ layout: 'fullscreen'
+ },
+ args: {
+ pageTabs: pageTabs,
+ showPageMenu: true,
+ showGlobalActions: true,
+ children: dummyContent
+ }
+};
+
+export const CustomGlobalActions: Story = {
+ parameters: {
+ layout: 'fullscreen'
+ },
+ args: {
+ pageTabs: pageTabs,
+ showPageMenu: true,
+ showGlobalActions: true,
+ children: dummyContent,
+ customGlobalActions: customGlobalActions
+ }
+};
+
+const simpleList =
+ Just a regular table footer}
+ rows={testRows(100)}
+ />
+ ;
+
+export const ExampleSimpleList: Story = {
+ parameters: {
+ layout: 'fullscreen'
+ },
+ name: 'Example: Simple List',
+ args: {
+ pageTabs: pageTabs,
+ showPageMenu: true,
+ showGlobalActions: true,
+ children: simpleList
+ }
+};
+
+const stickyList =
+ Sticky footer}
+ rows={testRows(40)}
+ stickyFooter
+ stickyHeader
+ />
+ ;
+
+export const ExampleStickyList: Story = {
+ parameters: {
+ layout: 'fullscreen'
+ },
+ name: 'Example: Sticky Header/Footer List',
+ args: {
+ pageTabs: pageTabs,
+ showPageMenu: true,
+ showGlobalActions: true,
+ children: stickyList
+ }
+};
+
+const examplePrimaryAction = {
+ alert('Clicked primary action');
+ }
+ }}
+ title='Members'
+ type='page'
+>
+ Sticky footer}
+ rows={testRows(40)}
+ stickyFooter
+ stickyHeader
+ />
+ ;
+
+export const ExamplePrimaryAction: Story = {
+ parameters: {
+ layout: 'fullscreen'
+ },
+ name: 'Example: Primary Action',
+ args: {
+ pageTabs: pageTabs,
+ showPageMenu: true,
+ showGlobalActions: true,
+ children: examplePrimaryAction
+ }
+};
+
+const exampleActionsContent = {
+ alert('Clicked primary action');
+ }
+ }}
+ title='Members'
+ type='page'
+>
+ Sticky footer}
+ rows={testRows(40)}
+ stickyFooter
+ stickyHeader
+ />
+ ;
+
+export const ExampleActions: Story = {
+ parameters: {
+ layout: 'fullscreen'
+ },
+ name: 'Example: Custom Actions',
+ args: {
+ pageTabs: pageTabs,
+ showPageMenu: true,
+ showGlobalActions: true,
+ children: exampleActionsContent
+ }
+};
+
+const mockIdeaCards = () => {
+ const cards = [];
+
+ for (let i = 0; i < 11; i++) {
+ cards.push(
+
+
+ {i % 3 === 0 && 'Sunset drinks cruise eat sleep repeat'}
+ {i % 3 === 1 && 'Elegance Rolls Royce on my private jet'}
+ {i % 3 === 2 && 'Down to the wire Bathurst 5000 Le Tour'}
+
+
+ {i % 3 === 0 && 'Numea captain’s table crystal waters paradise island the scenic route great adventure. Pirate speak the road less travelled seas the day '}
+ {i % 3 === 1 && 'Another day in paradise cruise life adventure bound gap year cruise time languid afternoons let the sea set you free'}
+ {i % 3 === 2 && No body text }
+
+
+ );
+ }
+ return cards;
+};
+
+const exampleCardViewContent = (
+
+
+ {mockIdeaCards()}
+
+
+);
+
+export const ExampleCardView: Story = {
+ parameters: {
+ layout: 'fullscreen'
+ },
+ name: 'Example: Card View',
+ args: {
+ pageTabs: pageTabs,
+ showPageMenu: true,
+ showGlobalActions: true,
+ children: exampleCardViewContent
+ }
+};
+
+const mockPosts = () => {
+ const posts = [];
+
+ for (let i = 0; i < 11; i++) {
+ posts.push(
+
+
+
+
+
+
+
+ {i % 3 === 0 && 'Sunset drinks cruise eat sleep repeat'}
+ {i % 3 === 1 && 'Elegance Rolls Royce on my private jet'}
+ {i % 3 === 2 && 'Down to the wire Bathurst 5000 Le Tour'}
+
+
+ {i % 3 === 0 && 'Numea captain’s table crystal waters paradise island the scenic route great adventure. Pirate speak the road less travelled seas the day '}
+ {i % 3 === 1 && 'Another day in paradise cruise life adventure bound gap year cruise time languid afternoons let the sea set you free'}
+ {i % 3 === 2 && 'Grand Prix gamble responsibly intensity is not a perfume The Datsun 180B Aerial ping pong knock for six watch with the boys total hospital pass.'}
+
+
+
+
+ 15%
+ viewed
+
+
+ 55%
+ opened
+
+
+
+
+
+ );
+ }
+ return posts;
+};
+
+const examplePostsContent = (
+
+
+ {<>{mockPosts()}>}
+
+
+);
+
+export const ExampleAlternativeList: Story = {
+ parameters: {
+ layout: 'fullscreen'
+ },
+ name: 'Example: Alternative List',
+ args: {
+ pageTabs: pageTabs,
+ showPageMenu: true,
+ showGlobalActions: true,
+ children: examplePostsContent
+ }
+};
+
+export const ExampleDetailScreen: Story = {
+ parameters: {
+ layout: 'fullscreen'
+ },
+ name: 'Example: Detail Page',
+ args: {
+ showPageMenu: true,
+ breadCrumbs: ,
+ showGlobalActions: true,
+ children: <>
+
+
+
+
+
Emerson Vaccaro
+
Colombus, OH
+
+
+
+
+
+
+
+ Last seen on 22 June 2023
+ Created on 27 Jan 2021
+
+
+ Emails received
+ 181
+
+
+ Emails opened
+ 104
+
+
+ Average open rate
+ 57%
+
+
+
+
+
+ Member data
+
+
+
+
Name
+
Emerson Vaccaro
+
+
+
Email
+
emerson@vaccaro.com
+
+
+
+
+
Newsletters
+
+
+
+ Daily news
+
+
+
+ Weekly roundup
+
+
+
+
+
Subscriptions
+
+ Gold — $12/month
+ Renews 21 Jan 2024
+
+
+
+
Activity
+
+ Logged in
+ Renews 21 Jan 2024
+
+
+ Subscribed to Daily News
+ Renews 21 Jan 2024
+
+
+
+
+ >
+ }
+};
\ No newline at end of file
diff --git a/apps/admin-x-design-system/src/global/layout/Page.tsx b/apps/admin-x-design-system/src/global/layout/Page.tsx
new file mode 100644
index 0000000000..f150a84036
--- /dev/null
+++ b/apps/admin-x-design-system/src/global/layout/Page.tsx
@@ -0,0 +1,100 @@
+import React from 'react';
+import {TabList} from '../TabView';
+import clsx from 'clsx';
+import PageMenu from './PageMenu';
+import GlobalActions from './GlobalActions';
+import Button from '../Button';
+import {BreadcrumbsProps} from '../Breadcrumbs';
+
+export interface PageTab {
+ id: string;
+ title: string;
+}
+
+export interface CustomGlobalAction {
+ iconName: string;
+ onClick?: () => void;
+}
+
+interface PageToolbarProps {
+ mainClassName?: string;
+ showPageMenu?: boolean;
+ showGlobalActions?: boolean;
+ customGlobalActions?: CustomGlobalAction[];
+ breadCrumbs?: React.ReactElement;
+ pageTabs?: PageTab[],
+ selectedTab?: string;
+ onTabChange?: (id: string) => void;
+ children?: React.ReactNode;
+}
+
+const PageToolbar: React.FC = ({
+ mainClassName,
+ showPageMenu = false,
+ showGlobalActions = false,
+ customGlobalActions,
+ breadCrumbs,
+ pageTabs,
+ selectedTab,
+ onTabChange,
+ children
+}) => {
+ const handleTabChange = (e: React.MouseEvent) => {
+ const newTab = e.currentTarget.id as string;
+ onTabChange!(newTab);
+ };
+
+ if (pageTabs?.length && !selectedTab) {
+ selectedTab = pageTabs[0].id;
+ }
+
+ const left: React.ReactNode =
+ {showPageMenu && (
+
+ )}
+ {breadCrumbs}
+ {pageTabs?.length && (
+
+ )}
+
+
;
+
+ mainClassName = clsx(
+ 'flex h-[calc(100%-72px)] w-[100vw] flex-auto flex-col',
+ mainClassName
+ );
+
+ const globalActions = (
+
+ {(customGlobalActions?.map((action) => {
+ return (
+
+ );
+ }))}
+ {showGlobalActions && }
+
+ );
+
+ return (
+
+
+ {left}
+ {globalActions}
+
+
+
+
+
+ );
+};
+
+export default PageToolbar;
\ No newline at end of file
diff --git a/apps/admin-x-design-system/src/global/layout/PageMenu.tsx b/apps/admin-x-design-system/src/global/layout/PageMenu.tsx
new file mode 100644
index 0000000000..a6ce8d929a
--- /dev/null
+++ b/apps/admin-x-design-system/src/global/layout/PageMenu.tsx
@@ -0,0 +1,12 @@
+import React from 'react';
+import Button from '../Button';
+
+const PageMenu: React.FC = () => {
+ return (
+ {
+ alert('Clicked on hamburger');
+ }} />
+ );
+};
+
+export default PageMenu;
\ No newline at end of file
diff --git a/apps/admin-x-design-system/src/global/layout/ViewContainer.stories.tsx b/apps/admin-x-design-system/src/global/layout/ViewContainer.stories.tsx
new file mode 100644
index 0000000000..957965e865
--- /dev/null
+++ b/apps/admin-x-design-system/src/global/layout/ViewContainer.stories.tsx
@@ -0,0 +1,172 @@
+import {useArgs} from '@storybook/preview-api';
+import type {Meta, StoryObj} from '@storybook/react';
+
+import ViewContainer, {PrimaryActionProps, ViewTab} from './ViewContainer';
+import Button from '../Button';
+import ButtonGroup from '../ButtonGroup';
+
+const meta = {
+ title: 'Global / Layout / View Container',
+ component: ViewContainer,
+ render: function Component(args) {
+ const [, updateArgs] = useArgs();
+
+ return {
+ updateArgs({selectedTab: tab});
+ args.onTabChange?.(tab);
+ }}
+ />;
+ },
+ tags: ['autodocs']
+} satisfies Meta;
+
+export default meta;
+type Story = StoryObj;
+
+const ContentContainer: React.FC<{children: React.ReactNode}> = ({
+ children
+}) => {
+ return {children}
;
+};
+
+export const exampleActions = [
+ {
+ alert('Clicked filter');
+ }} />,
+ {
+ alert('Clicked sort');
+ }} />,
+ {
+ alert('Clicked search');
+ }} />,
+ {
+ alert('Clicked list view');
+ }
+ },
+ {
+ icon: 'cardview',
+ size: 'sm',
+ link: true,
+ iconColorClass: 'text-grey-500',
+ onClick: () => {
+ alert('Clicked card view');
+ }
+ }
+ ]} />
+];
+
+const primaryAction: PrimaryActionProps = {
+ title: 'Add item',
+ color: 'black',
+ onClick: () => {
+ alert('Clicked primary action');
+ }
+};
+
+const tabs: ViewTab[] = [
+ {
+ id: 'steph',
+ title: 'Steph Curry',
+ contents: The tabs component lets you add various datasets. It uses the `TabList`
component to stay consistent with the simple TabView.
+ },
+ {
+ id: 'klay',
+ title: 'Klay Thompson',
+ contents: Splash brother #11.
+ }
+];
+
+export const Default: Story = {
+ args: {
+ type: 'page',
+ toolbarBorder: false,
+ children: The view container component is the main container of pages and/or sections on a page. Select one of the stories on the right to browse use cases.
+ }
+};
+
+export const PageType: Story = {
+ name: 'Type: Page',
+ args: {
+ type: 'page',
+ title: 'Page title',
+ children: In its simplest form you can use this component as the main container of pages.
+ }
+};
+
+export const SectionType: Story = {
+ name: 'Type: Section',
+ args: {
+ type: 'section',
+ title: 'Section title',
+ children: This example shows how to use it for sections on a page.
+ }
+};
+
+export const PrimaryActionOnPage: Story = {
+ args: {
+ type: 'page',
+ title: 'Page title',
+ primaryAction: primaryAction
+ }
+};
+
+export const ActionsOnPage: Story = {
+ args: {
+ type: 'page',
+ title: 'Page title',
+ actions: exampleActions,
+ primaryAction: primaryAction
+ }
+};
+
+export const PrimaryActionOnSection: Story = {
+ args: {
+ type: 'section',
+ title: 'Section title',
+ primaryAction: primaryAction
+ }
+};
+
+export const MultipleTabs: Story = {
+ args: {
+ type: 'section',
+ title: 'Section title',
+ tabs: tabs
+ }
+};
+
+export const TabsWithPrimaryAction: Story = {
+ args: {
+ type: 'section',
+ title: 'Section title',
+ tabs: tabs,
+ primaryAction: primaryAction
+ }
+};
+
+export const TabsWithActions: Story = {
+ args: {
+ type: 'section',
+ title: 'Section title',
+ tabs: tabs,
+ primaryAction: primaryAction,
+ actions: exampleActions
+ }
+};
+
+export const HiddenActions: Story = {
+ args: {
+ type: 'section',
+ title: 'Hover to show actions',
+ tabs: tabs,
+ actions: exampleActions,
+ actionsHidden: true
+ }
+};
\ No newline at end of file
diff --git a/apps/admin-x-design-system/src/global/layout/ViewContainer.tsx b/apps/admin-x-design-system/src/global/layout/ViewContainer.tsx
new file mode 100644
index 0000000000..2a45c39214
--- /dev/null
+++ b/apps/admin-x-design-system/src/global/layout/ViewContainer.tsx
@@ -0,0 +1,197 @@
+import React from 'react';
+import {Tab, TabList} from '../TabView';
+import Heading from '../Heading';
+import clsx from 'clsx';
+import Button, {ButtonColor, ButtonProps} from '../Button';
+import {ButtonGroupProps} from '../ButtonGroup';
+import DynamicTable, {DynamicTableProps} from '../table/DynamicTable';
+
+export interface View {
+ id: string;
+ buttonClasses?: string;
+ buttonChildren: React.ReactNode;
+ contents: React.ReactNode;
+}
+
+export interface ViewTab extends Tab {
+ views?: View[];
+}
+
+export interface PrimaryActionProps {
+ title?: string;
+ icon?: string;
+ color?: ButtonColor;
+ onClick?: () => void;
+}
+
+interface ViewContainerProps {
+ type: 'page' | 'section';
+ title?: string;
+ tabs?: ViewTab[];
+ selectedTab?: string;
+ selectedView?: string;
+ onTabChange?: (id: string) => void;
+ mainContainerClassName?: string;
+ toolbarWrapperClassName?: string;
+ toolbarContainerClassName?: string;
+ toolbarLeftClassName?: string;
+ toolbarBorder?: boolean;
+ primaryAction?: PrimaryActionProps;
+ actions?: (React.ReactElement | React.ReactElement)[];
+ actionsClassName?: string;
+ actionsHidden?: boolean;
+ contentWrapperClassName?: string;
+ contentFullBleed?: boolean;
+ children?: React.ReactNode;
+}
+
+const ViewContainer: React.FC = ({
+ type,
+ title,
+ tabs,
+ selectedTab,
+ onTabChange,
+ mainContainerClassName,
+ toolbarWrapperClassName,
+ toolbarContainerClassName,
+ toolbarLeftClassName,
+ primaryAction,
+ actions,
+ actionsClassName,
+ actionsHidden,
+ toolbarBorder = true,
+ contentWrapperClassName,
+ contentFullBleed = false,
+ children
+}) => {
+ let toolbar = <>>;
+ let mainContent:React.ReactNode = <>>;
+
+ const handleTabChange = (e: React.MouseEvent) => {
+ const newTab = e.currentTarget.id as string;
+ onTabChange!(newTab);
+ };
+
+ let isSingleDynamicTable;
+ let singleDynamicTableIsSticky = false;
+
+ if (tabs?.length && !children) {
+ if (!selectedTab) {
+ selectedTab = tabs[0].id;
+ }
+
+ mainContent = <>
+ {tabs.map((tab) => {
+ return (
+ <>
+ {tab.contents &&
+
+ }
+ >
+ );
+ })}
+ >;
+ } else if (React.isValidElement(children) && children.type === DynamicTable) {
+ isSingleDynamicTable = true;
+ const dynTable = (children as React.ReactElement);
+ if (dynTable.props.stickyHeader || dynTable.props.stickyFooter) {
+ singleDynamicTableIsSticky = true;
+ children = isSingleDynamicTable
+ ? React.cloneElement(dynTable, {
+ ...(dynTable.props as DynamicTableProps),
+ singlePageTable: true
+ })
+ : children;
+ }
+ mainContent = children;
+ } else {
+ mainContent = children;
+ }
+
+ toolbarWrapperClassName = clsx(
+ 'z-50',
+ type === 'page' && 'sticky top-18 mx-auto w-full max-w-7xl bg-white px-12 pt-[3vmin]',
+ toolbarContainerClassName
+ );
+
+ toolbarContainerClassName = clsx(
+ 'flex justify-between',
+ toolbarBorder && 'border-b border-grey-200',
+ toolbarContainerClassName
+ );
+
+ toolbarLeftClassName = clsx(
+ 'flex flex-col',
+ toolbarLeftClassName
+ );
+
+ actionsClassName = clsx(
+ 'flex items-center gap-10 transition-all',
+ actionsHidden && 'opacity-0 group-hover/view-container:opacity-100',
+ tabs?.length ? 'pb-2' : 'pb-3',
+ actionsClassName
+ );
+
+ if (primaryAction) {
+ primaryAction!.color = 'black';
+ }
+
+ const primaryActionContents = <>
+ {primaryAction?.title && (
+
+ )}
+ >;
+
+ toolbar = (
+
+
+
+ {title && {title} }
+ {tabs?.length && (
+
+ )}
+
+
+ {actions}
+ {primaryActionContents}
+
+
+
+ );
+
+ mainContainerClassName = clsx(
+ 'group/view-container flex flex-auto flex-col',
+ mainContainerClassName
+ );
+
+ if (singleDynamicTableIsSticky) {
+ contentFullBleed = true;
+ }
+
+ contentWrapperClassName = clsx(
+ 'relative mx-auto w-full flex-auto',
+ !contentFullBleed && 'max-w-7xl px-12',
+ contentWrapperClassName,
+ (!title && !actions) && 'pt-[3vmin]'
+ );
+
+ return (
+
+ {(title || actions) && toolbar}
+
+ {mainContent}
+
+
+ );
+};
+
+export default ViewContainer;
\ No newline at end of file
diff --git a/apps/admin-x-design-system/src/global/table/DynamicTable.stories.tsx b/apps/admin-x-design-system/src/global/table/DynamicTable.stories.tsx
new file mode 100644
index 0000000000..03d8d80cb3
--- /dev/null
+++ b/apps/admin-x-design-system/src/global/table/DynamicTable.stories.tsx
@@ -0,0 +1,211 @@
+import type {Meta, StoryObj} from '@storybook/react';
+
+import DynamicTable, {DynamicTableColumn, DynamicTableRow} from './DynamicTable';
+import Avatar from '../Avatar';
+import Hint from '../Hint';
+import Pagination from '../Pagination';
+import Button from '../Button';
+
+const meta = {
+ title: 'Global / Table / Dynamic Table',
+ component: DynamicTable,
+ tags: ['autodocs']
+} satisfies Meta;
+
+export default meta;
+type Story = StoryObj;
+
+export const testColumns: DynamicTableColumn[] = [
+ {
+ title: 'Member'
+ },
+ {
+ title: 'Status'
+ },
+ {
+ title: 'Open rate'
+ },
+ {
+ title: 'Location',
+ noWrap: true
+ },
+ {
+ title: 'Created',
+ noWrap: true
+ },
+ {
+ title: 'Signed up on post',
+ noWrap: true,
+ maxWidth: '150px'
+ },
+ {
+ title: 'Newsletter'
+ },
+ {
+ title: 'Billing period'
+ },
+ {
+ title: 'Email sent'
+ },
+ {
+ title: '',
+ hidden: true,
+ disableRowClick: true
+ }
+];
+
+export const testRows = (noOfRows: number) => {
+ const data: DynamicTableRow[] = [];
+ for (let i = 0; i < noOfRows; i++) {
+ data.push(
+ {
+ onClick: () => {
+ alert('Clicked on row: ' + i);
+ },
+ cells: [
+ (
+ {i % 3 === 0 &&
}
+ {i % 3 === 1 &&
}
+ {i % 3 === 2 &&
}
+
+ {i % 3 === 0 &&
Jamie Larson
}
+ {i % 3 === 1 &&
Giana Septimus
}
+ {i % 3 === 2 &&
Zaire Bator
}
+
jamie@larson.com
+
+
),
+ 'Free',
+ '40%',
+ 'London, UK',
+ '22 June 2023',
+ 'Lorem ipsum dolor sit amet, consectetur adipiscing elit',
+ 'Subscribed',
+ 'Monthly',
+ '1,303',
+ {
+ alert('Clicked Edit in row:' + i);
+ }} />
+ ]
+ }
+ );
+ }
+ return data;
+};
+
+/**
+ * In its simplest form this component lets you create a table with passing a
+ * `columns` and `rows` parameter. You can customise each column's width, whether
+ * it should wrap etc.
+ */
+export const Default: Story = {
+ args: {
+ columns: testColumns,
+ rows: testRows(10)
+ }
+};
+
+export const HiddenHeader: Story = {
+ args: {
+ columns: testColumns,
+ rows: testRows(10),
+ hideHeader: true
+ }
+};
+
+export const NoBorder: Story = {
+ args: {
+ columns: testColumns,
+ rows: testRows(10),
+ border: false
+ }
+};
+
+/**
+ * By default it's just a simple table but you can set its header or footer to
+ * be sticky. In this case the container is `absolute` positioned with `inset-0`
+ * so the size and layout of the table is completely controlled by its container.
+ */
+export const StickyHeader: Story = {
+ args: {
+ stickyHeader: true,
+ columns: testColumns,
+ rows: testRows(40)
+ }
+};
+
+export const StickyFooter: Story = {
+ args: {
+ stickyFooter: true,
+ footer: Here we go ,
+ columns: testColumns,
+ rows: testRows(40)
+ }
+};
+
+export const AllSticky: Story = {
+ // render: () => (
+ // Table footer} rows={rows(40)} stickyFooter stickyHeader />
+ // )
+ args: {
+ stickyHeader: true,
+ stickyFooter: true,
+ footer: Here we go ,
+ columns: testColumns,
+ rows: testRows(40)
+ }
+};
+
+export const HalfPageExample: Story = {
+ decorators: [(_story: () => React.ReactNode) => (
+
+
+
+
Half page example
+
This example shows how the table can positioned on the page by its container. You can enable this mode by setting `absolute=true` or by enabling `stickyHeader` or `stickyFooter` (in these cases the component switches to `display: absolute`).
+
If you use the table like this, make sure to set the container to `display:relative`.
+
+
{_story()}
+
+
+ )],
+ args: {
+ stickyHeader: true,
+ stickyFooter: true,
+ columns: testColumns,
+ rows: testRows(40),
+ footer: This is a table footer
+ }
+};
+
+export const FullPageExample: Story = {
+ decorators: [(_story: () => React.ReactNode) => (
+
+
+
Page title
+
This example shows how you can create a page with arbitrary content on the top and a large table at the bottom that fills up the remaining space. The table has a sticky header row, a footer that is always visible and scrolling vertically and horizontally (resize the window to see the effect).
+
The size and positioning of the table is completely controlled by its container . The container must have `relative` position. Use a column flexbox as the main container of the page then set the table container to flex-auto to fill the available horizontal space.
+
{_story()}
+
+
+ )],
+ args: {
+ stickyHeader: true,
+ stickyFooter: true,
+ columns: testColumns,
+ rows: testRows(40),
+ tableContainerClassName: 'px-10',
+ footerClassName: 'mx-10',
+ footer: This is a table footer
+ }
+};
+
+export const PaginationExample: Story = {
+ args: {
+ columns: testColumns,
+ rows: testRows(10),
+ footer:
+
Table footer comes here
+
{}} page={1} pages={5} prevPage={() => {}} setPage={() => {}} total={15} />
+
+ }
+};
\ No newline at end of file
diff --git a/apps/admin-x-design-system/src/global/table/DynamicTable.tsx b/apps/admin-x-design-system/src/global/table/DynamicTable.tsx
new file mode 100644
index 0000000000..cd4986f9cd
--- /dev/null
+++ b/apps/admin-x-design-system/src/global/table/DynamicTable.tsx
@@ -0,0 +1,228 @@
+import React from 'react';
+import {Heading} from '../..';
+import clsx from 'clsx';
+import {tableRowHoverBgClasses} from '../TableRow';
+
+export type DynamicTableColumn = {
+ title: string;
+ minWidth?: string;
+ maxWidth?: string;
+ noWrap?: boolean;
+ align?: 'left' | 'center' | 'right';
+ valign?: 'top' | 'middle' | 'bottom';
+ hidden?: boolean;
+ disableRowClick?: boolean;
+}
+
+export type DynamicTableRow = {
+ cells: React.ReactNode[];
+ onClick?: () => void;
+}
+
+export interface DynamicTableProps {
+ columns: DynamicTableColumn[];
+ rows: DynamicTableRow[];
+ horizontalScrolling?: boolean;
+ absolute?: boolean;
+ stickyHeader?: boolean;
+ hideHeader?: boolean;
+ headerBorder?: boolean;
+
+ /**
+ * Set this parameter if the table is the main content in a viewcontainer or on a page
+ */
+ singlePageTable?: boolean;
+
+ border?: boolean;
+ footerBorder?:boolean;
+ footer?: React.ReactNode;
+ stickyFooter?: boolean;
+ containerClassName?: string;
+ tableContainerClassName?: string;
+ tableClassName?: string;
+ thClassName?: string;
+ tdClassName?: string;
+ cellClassName?: string;
+ trClassName?: string;
+ footerClassName?: string;
+}
+
+const DynamicTable: React.FC = ({
+ columns,
+ rows,
+ horizontalScrolling = false,
+ absolute = false,
+ stickyHeader = false,
+ hideHeader = false,
+ headerBorder = true,
+ border = true,
+ footer,
+ footerBorder = true,
+ stickyFooter = false,
+ singlePageTable = false,
+ containerClassName,
+ tableContainerClassName,
+ tableClassName,
+ thClassName,
+ tdClassName,
+ cellClassName,
+ trClassName,
+ footerClassName
+}) => {
+ let headerColID = 0;
+ let rowID = 0;
+
+ containerClassName = clsx(
+ 'flex max-h-full w-full flex-col',
+ (stickyHeader || stickyFooter || absolute) ? 'absolute inset-0' : 'relative',
+ containerClassName
+ );
+
+ tableContainerClassName = clsx(
+ 'flex-auto overflow-x-auto',
+ !horizontalScrolling && 'w-full max-w-full',
+ (singlePageTable && (stickyHeader || stickyFooter || absolute)) && 'px-12 xl:px-[calc((100%-1280px)/2+48px)]',
+ tableContainerClassName
+ );
+
+ tableClassName = clsx(
+ 'h-full max-h-full min-w-full flex-auto table-fixed',
+ tableClassName
+ );
+
+ thClassName = clsx(
+ 'bg-white py-3 pr-3 text-left',
+ thClassName
+ );
+
+ tdClassName = clsx(
+ 'w-full border-b group-hover:border-grey-200',
+ border ? 'border-grey-200' : 'border-transparent',
+ tdClassName
+ );
+
+ cellClassName = clsx(
+ 'flex h-full py-3 pr-3',
+ cellClassName
+ );
+
+ trClassName = clsx(
+ 'group',
+ tableRowHoverBgClasses,
+ trClassName
+ );
+
+ footerClassName = clsx(
+ 'bg-white',
+ (singlePageTable && stickyFooter) && 'mx-12 xl:mx-[calc((100%-1280px)/2+48px)]',
+ footer && 'py-3',
+ stickyFooter && 'sticky inset-x-0 bottom-0',
+ footerBorder && 'border-t border-grey-200',
+ footerClassName
+ );
+
+ const footerContents = ;
+
+ return (
+ // Outer container for testing. Should not be part of the table component
+ //
+
+
+
+
+ {!hideHeader &&
+
+
+ {columns.map((column) => {
+ headerColID = headerColID + 1;
+ const thMaxWidth: string = column.maxWidth || 'auto';
+ const thMinWidth: string = column.minWidth || 'auto';
+ const thStyles = {
+ maxWidth: thMaxWidth,
+ minWidth: thMinWidth,
+ width: thMaxWidth
+ };
+ return (
+
+ {column.title}
+ );
+ })}
+
+ {headerBorder && (
+
+
+
+ )}
+
+ }
+
+ {rows.map((row) => {
+ let colID = 0;
+ rowID = rowID + 1;
+ return
+ {row.cells.map((cell) => {
+ const currentColumn: DynamicTableColumn = columns[colID] || {title: ''};
+
+ let customTdClasses = tdClassName;
+ customTdClasses = clsx(
+ customTdClasses,
+ currentColumn.noWrap ? 'truncate' : '',
+ currentColumn.align === 'center' && 'text-center',
+ currentColumn.align === 'right' && 'text-right'
+ );
+
+ if (rowID === rows.length && footerBorder) {
+ customTdClasses = clsx(
+ customTdClasses,
+ 'border-none'
+ );
+ }
+
+ const tdMaxWidth: string = (currentColumn !== undefined && currentColumn.maxWidth) || 'auto';
+ const tdMinWidth: string = (currentColumn !== undefined && currentColumn.minWidth) || 'auto';
+ const tdStyles = {
+ maxWidth: tdMaxWidth,
+ minWidth: tdMinWidth,
+ width: tdMaxWidth
+ };
+ let customCellClasses = cellClassName;
+ customCellClasses = clsx(
+ customCellClasses,
+ currentColumn.valign === 'middle' || !currentColumn.valign && 'items-center',
+ currentColumn.valign === 'top' && 'items-start',
+ currentColumn.valign === 'bottom' && 'items-end'
+ );
+ if (row.onClick && !currentColumn.disableRowClick) {
+ customCellClasses = clsx(
+ customCellClasses,
+ 'cursor-pointer'
+ );
+ }
+ if (currentColumn.hidden) {
+ customCellClasses = clsx(
+ customCellClasses,
+ 'opacity-0 group-hover:opacity-100'
+ );
+ }
+ const data = (
+
+ {})}>{cell}
+
+ );
+ colID = colID + 1;
+ return data;
+ })}
+ ;
+ })}
+
+
+ {!stickyFooter && footerContents}
+
+ {stickyFooter && footerContents}
+
+
+ //
+ );
+};
+
+export default DynamicTable;
\ No newline at end of file