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 && - + ); +}; + +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 => ( -
- -
- ))} -
+ {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 ( +
+
+ ); + } + 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
+
+
+ Labels +
VIP
+
+
+
+ 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 ( +
+ ); + + return ( +
+
+ +
{globalActions}
+
+
+
+ {children} +
+
+
+ ); +}; + +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 ( +