Admin X demo app content (#19079)

refs. https://github.com/TryGhost/Product/issues/4169

- Added demo content for POC AdminX demo app
This commit is contained in:
Peter Zimon 2023-11-22 13:44:39 +01:00 committed by GitHub
parent 8d0b9cd269
commit 6b46c828e7
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
19 changed files with 599 additions and 147 deletions

View File

@ -1,12 +1,244 @@
import {Button} from '@tryghost/admin-x-design-system';
import {Avatar, Button, ButtonGroup, DynamicTable, DynamicTableColumn, DynamicTableRow, Heading, Hint, Page, SortMenu, ViewContainer} from '@tryghost/admin-x-design-system';
import {useRouting} from '@tryghost/admin-x-framework/routing';
import {useState} from 'react';
const MainContent = () => {
const {updateRoute} = useRouting();
const [view, setView] = useState<string>('list');
return <div>
<Button label='Open modal' onClick={() => updateRoute('demo-modal')} />
</div>;
const dummyActions = [
<Button label='Filter' onClick={() => {
alert('Clicked filter');
}} />,
<SortMenu
direction='desc'
items={[
{
id: 'date-added',
label: 'Date added',
selected: true
},
{
id: 'name',
label: 'Name'
},
{
id: 'redemptions',
label: 'Open Rate'
}
]}
position="left"
onDirectionChange={() => {}}
onSortChange={() => {}}
/>,
<Button icon='magnifying-glass' size='sm' onClick={() => {
alert('Clicked search');
}} />,
<ButtonGroup buttons={[
{
icon: 'listview',
size: 'sm',
iconColorClass: (view === 'list' ? 'text-black' : 'text-grey-500'),
onClick: () => {
setView('list');
}
},
{
icon: 'cardview',
size: 'sm',
iconColorClass: (view === 'card' ? 'text-black' : 'text-grey-500'),
onClick: () => {
setView('card');
}
}
]} clearBg={false} link />
];
const testColumns: DynamicTableColumn[] = [
{
title: 'Member',
noWrap: true,
minWidth: '1%',
maxWidth: '1%'
},
{
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
}
];
const testRows = (noOfRows: number) => {
const data: DynamicTableRow[] = [];
for (let i = 0; i < noOfRows; i++) {
data.push(
{
onClick: () => {
alert('Clicked on row: ' + i);
},
cells: [
(<div className='flex items-center gap-3 whitespace-nowrap pr-10'>
<Avatar image={`https://i.pravatar.cc/150?img=${i}`} />
<div>
{i % 3 === 0 && <div className='whitespace-nowrap text-md'>Jamie Larson</div>}
{i % 3 === 1 && <div className='whitespace-nowrap text-md'>Giana Septimus</div>}
{i % 3 === 2 && <div className='whitespace-nowrap text-md'>Zaire Bator</div>}
<div className='text-grey-700'>jamie@larson.com</div>
</div>
</div>),
'Free',
'40%',
'London, UK',
<div>
<div>22 June 2023</div>
<div className='text-grey-500'>5 months ago</div>
</div>,
'Lorem ipsum dolor sit amet, consectetur adipiscing elit',
'Subscribed',
'Monthly',
'1,303',
<Button color='green' label='Edit' link onClick={() => {
alert('Clicked Edit in row:' + i);
}} />
]
}
);
}
return data;
};
const dummyCards = (noOfCards: number) => {
const cards = [];
for (let i = 0; i < noOfCards; i++) {
cards.push(
<div className='flex min-h-[20vh] cursor-pointer flex-col items-center gap-5 rounded-sm bg-grey-100 p-7 pt-9 transition-all hover:bg-grey-200' onClick={() => {
alert('Clicked');
}}>
<Avatar image={`https://i.pravatar.cc/150?img=${i}`} size='xl' />
<div className='flex flex-col items-center'>
<Heading level={5}>
{i % 3 === 0 && 'Jamie Larson'}
{i % 3 === 1 && 'Giana Septimus'}
{i % 3 === 2 && 'Zaire Bator'}
</Heading>
<div className='mt-1 text-sm text-grey-700'>
{i % 3 === 0 && 'jamie@larson.com'}
{i % 3 === 1 && 'giana@septimus.com'}
{i % 3 === 2 && 'zaire@bator.com'}
</div>
</div>
<div className='flex w-full flex-col gap-4 border-t border-grey-300 pt-5'>
{i % 3 === 0 && (<>
<div className='flex gap-4'>
<div className='basis-1/2 text-center'>
<Heading level={6}>Open rate</Heading>
<div className='text-lg'>83%</div>
</div>
<div className='basis-1/2 text-center'>
<Heading level={6}>Click rate</Heading>
<div className='text-lg'>19%</div>
</div>
</div>
</>)}
{i % 3 === 1 && (<>
<div className='flex gap-4'>
<div className='basis-1/2 text-center'>
<Heading level={6}>Open rate</Heading>
<div className='text-lg'>68%</div>
</div>
<div className='basis-1/2 text-center'>
<Heading level={6}>Click rate</Heading>
<div className='text-lg'>21%</div>
</div>
</div>
</>)}
{i % 3 === 2 && (<>
<div className='flex gap-4'>
<div className='basis-1/2 text-center'>
<Heading level={6}>Open rate</Heading>
<div className='text-lg'>89%</div>
</div>
<div className='basis-1/2 text-center'>
<Heading level={6}>Click rate</Heading>
<div className='text-lg'>34%</div>
</div>
</div>
</>)}
</div>
</div>
);
}
return cards;
};
let contents = <></>;
switch (view) {
case 'list':
contents = <DynamicTable
cellClassName='text-sm'
columns={testColumns}
footer={
<Hint>30 members</Hint>
}
rows={testRows(30)}
stickyFooter
stickyHeader
/>;
break;
case 'card':
contents = <div className='grid grid-cols-4 gap-8 py-8'>{dummyCards(30)}</div>;
break;
}
const demoPage = (
<Page>
<ViewContainer
actions={dummyActions}
primaryAction={{
title: 'About',
onClick: () => {
updateRoute('demo-modal');
}
}}
title='AdminX Demo App'
toolbarBorder={view === 'card'}
type='page'
>
{contents}
</ViewContainer>
</Page>
);
return demoPage;
};
export default MainContent;

View File

@ -1,18 +1,30 @@
import NiceModal from '@ebay/nice-modal-react';
import {Modal} from '@tryghost/admin-x-design-system';
import {Heading, Modal} from '@tryghost/admin-x-design-system';
import {useRouting} from '@tryghost/admin-x-framework/routing';
const DemoModal = NiceModal.create(() => {
const {updateRoute} = useRouting();
const modal = NiceModal.useModal();
return (
<Modal
afterClose={() => {
updateRoute('');
}}
title='Demo modal'
cancelLabel=''
okLabel='Close'
title='About'
onOk={() => {
updateRoute('');
modal.remove();
}}
>
Demo modal
<div className='mt-3 flex flex-col gap-4'>
<p>{`You're looking at a React app inside Ghost Admin. It uses common AdminX framework and Design System packages, and works seamlessly with the current Admin's routing.`}</p>
<p>{`At the moment the look and feel follows the current Admin's style to blend in with existing pages. However the system is built in a very flexible way to allow easy updates in the future.`}</p>
<Heading className='-mb-2 mt-4' level={5}>Contents</Heading>
<p>{`The demo uses a mocked list of members — it's `}<strong>not</strong> {`the actual or future design of members in Ghost Admin. Instead, the pages showcase common design patterns like a list and detail, navigation, modals and toasts.`}</p>
</div>
</Modal>
);
});

View File

@ -241,3 +241,7 @@ html, body, #root {
grid-template-columns: auto 240px;
gap: 32px;
}
.sbdocs-a {
color: #30CF43 !important;
}

View File

@ -150,6 +150,19 @@
text-indent: 0; /* 1 */
border-color: inherit; /* 2 */
border-collapse: collapse; /* 3 */
margin: 0;
width: auto;
max-width: auto;
}
table td, table th {
padding: unset;
vertical-align: middle;
text-align: left;
line-height: auto;
-webkit-user-select: text;
-moz-user-select: text;
user-select: text;
}
/*
@ -193,7 +206,7 @@
*/
button,
[type='button'],
/* [type='button'], */
[type='reset'],
[type='submit'] {
-webkit-appearance: button; /* 1 */
@ -201,6 +214,8 @@
background-image: none; /* 2 */
}
/*
Use the modern Firefox focus style for all focusable elements.
*/

View File

@ -32,7 +32,7 @@ const Button: React.FC<ButtonProps> = ({
label = '',
hideLabel = false,
icon = '',
iconColorClass = 'text-black',
iconColorClass,
color = 'clear',
fullWidth,
link,
@ -67,6 +67,7 @@ const Button: React.FC<ButtonProps> = ({
className
);
loadingIndicatorColor = 'light';
iconColorClass = iconColorClass || 'text-white';
break;
case 'grey':
className = clsx(
@ -81,6 +82,7 @@ const Button: React.FC<ButtonProps> = ({
className
);
loadingIndicatorColor = 'light';
iconColorClass = iconColorClass || 'text-white';
break;
case 'red':
className = clsx(
@ -88,6 +90,7 @@ const Button: React.FC<ButtonProps> = ({
className
);
loadingIndicatorColor = 'light';
iconColorClass = iconColorClass || 'text-white';
break;
case 'white':
className = clsx(

View File

@ -6,7 +6,8 @@ import {ButtonProps} from './Button';
const ButtonGroupMeta = {
title: 'Global / Button Group',
component: ButtonGroup,
tags: ['autodocs']
tags: ['autodocs'],
decorators: [(_story: () => React.ReactNode) => (<div className='inline-block'>{_story()}</div>)]
} satisfies Meta<typeof ButtonGroup>;
export default ButtonGroupMeta;
@ -50,3 +51,20 @@ export const LinkButtons: Story = {
link: true
}
};
export const WithBackground: Story = {
args: {
buttons: linkButtons,
link: true,
clearBg: false
}
};
export const SmallWithBackground: Story = {
args: {
buttons: linkButtons,
link: true,
clearBg: false,
size: 'sm'
}
};

View File

@ -1,18 +1,35 @@
import React from 'react';
import Button from './Button';
import Button, {ButtonSize} from './Button';
import {ButtonProps} from './Button';
import clsx from 'clsx';
export interface ButtonGroupProps {
size?: ButtonSize;
buttons: Array<ButtonProps>;
link?: boolean;
linkWithPadding?: boolean;
clearBg?: boolean;
className?: string;
}
const ButtonGroup: React.FC<ButtonGroupProps> = ({buttons, link, linkWithPadding, className}) => {
const ButtonGroup: React.FC<ButtonGroupProps> = ({size = 'md', buttons, link, linkWithPadding, clearBg = true, className}) => {
let groupColorClasses = clsx(
'flex items-center justify-start rounded',
link ? 'gap-4' : 'gap-5',
className
);
if (link && !clearBg) {
groupColorClasses = clsx(
'transition-all hover:bg-grey-200 dark:hover:bg-grey-900',
size === 'sm' ? 'h-7 px-3' : 'h-[34px] px-4',
groupColorClasses
);
}
return (
<div className={`flex items-center ${link ? 'gap-5' : 'gap-3'} ${className}`}>
<div className={groupColorClasses}>
{buttons.map(({key, ...props}) => (
<Button key={key} link={link} linkWithPadding={linkWithPadding} {...props} />
))}

View File

@ -6,7 +6,12 @@ import Tooltip from './Tooltip';
const meta = {
title: 'Global / Tooltip',
component: Tooltip,
tags: ['autodocs']
tags: ['autodocs'],
decorators: [(_story: () => React.ReactNode) => (
<div className='p-10'>
{_story()}
</div>
)]
} satisfies Meta<typeof Tooltip>;
export default meta;

View File

@ -20,6 +20,9 @@ const meta = {
title: 'Global / Layout / Page',
component: Page,
tags: ['autodocs'],
parameters: {
layout: 'fullscreen'
},
render: function Component(args) {
const [, updateArgs] = useArgs();
@ -35,7 +38,7 @@ const meta = {
export default meta;
type Story = StoryObj<typeof Page>;
const dummyContent = <div className='m-auto max-w-[800px] p-5 text-center'>Placeholder content</div>;
const dummyContent = <div className='w-full bg-grey-100 p-5 text-center'>Placeholder content</div>;
const customGlobalActions: CustomGlobalAction[] = [
{
@ -58,52 +61,66 @@ const pageTabs: Tab[] = [
];
export const Default: Story = {
parameters: {
layout: 'fullscreen'
},
args: {
pageTabs: pageTabs,
children: dummyContent
}
};
export const WithHamburger: Story = {
parameters: {
layout: 'fullscreen'
},
export const LimitToolbarWidth: Story = {
args: {
pageTabs: pageTabs,
showPageMenu: true,
children: dummyContent,
fullBleedToolbar: false
}
};
export const WithHamburger: Story = {
args: {
pageTabs: pageTabs,
showAppMenu: true,
children: dummyContent
}
};
export const WithGlobalActions: Story = {
parameters: {
layout: 'fullscreen'
},
args: {
pageTabs: pageTabs,
showPageMenu: true,
showAppMenu: true,
showGlobalActions: true,
children: dummyContent
}
};
export const CustomGlobalActions: Story = {
parameters: {
layout: 'fullscreen'
},
args: {
pageTabs: pageTabs,
showPageMenu: true,
showAppMenu: true,
showGlobalActions: true,
children: dummyContent,
customGlobalActions: customGlobalActions
}
};
const currentAdminExample = <ViewContainer
title='Members'
type='page'
>
<DynamicTable
columns={testColumns}
rows={testRows(100)}
/>
</ViewContainer>;
export const ExampleCurrentAdminList: Story = {
name: 'Example: List in Current Admin',
args: {
children: currentAdminExample
}
};
const simpleList = <ViewContainer
firstOnPage={false}
title='Members'
type='page'
>
@ -115,19 +132,17 @@ const simpleList = <ViewContainer
</ViewContainer>;
export const ExampleSimpleList: Story = {
parameters: {
layout: 'fullscreen'
},
name: 'Example: Simple List',
args: {
pageTabs: pageTabs,
showPageMenu: true,
showAppMenu: true,
showGlobalActions: true,
children: simpleList
}
};
const stickyList = <ViewContainer
firstOnPage={false}
title='Members'
type='page'
>
@ -141,19 +156,17 @@ const stickyList = <ViewContainer
</ViewContainer>;
export const ExampleStickyList: Story = {
parameters: {
layout: 'fullscreen'
},
name: 'Example: Sticky Header/Footer List',
args: {
pageTabs: pageTabs,
showPageMenu: true,
showAppMenu: true,
showGlobalActions: true,
children: stickyList
}
};
const examplePrimaryAction = <ViewContainer
firstOnPage={false}
primaryAction={{
title: 'Add member',
color: 'black',
@ -174,13 +187,10 @@ const examplePrimaryAction = <ViewContainer
</ViewContainer>;
export const ExamplePrimaryAction: Story = {
parameters: {
layout: 'fullscreen'
},
name: 'Example: Primary Action',
args: {
pageTabs: pageTabs,
showPageMenu: true,
showAppMenu: true,
showGlobalActions: true,
children: examplePrimaryAction
}
@ -188,6 +198,7 @@ export const ExamplePrimaryAction: Story = {
const exampleActionsContent = <ViewContainer
actions={exampleActionButtons}
firstOnPage={false}
primaryAction={{
title: 'Add member',
icon: 'add',
@ -209,13 +220,10 @@ const exampleActionsContent = <ViewContainer
</ViewContainer>;
export const ExampleActions: Story = {
parameters: {
layout: 'fullscreen'
},
name: 'Example: Custom Actions',
args: {
pageTabs: pageTabs,
showPageMenu: true,
showAppMenu: true,
showGlobalActions: true,
children: exampleActionsContent
}
@ -246,6 +254,7 @@ const mockIdeaCards = () => {
const exampleCardViewContent = (
<ViewContainer
actions={exampleActionButtons}
firstOnPage={false}
primaryAction={{
title: 'New idea',
icon: 'add'
@ -260,13 +269,10 @@ const exampleCardViewContent = (
);
export const ExampleCardView: Story = {
parameters: {
layout: 'fullscreen'
},
name: 'Example: Card View',
args: {
pageTabs: pageTabs,
showPageMenu: true,
showAppMenu: true,
showGlobalActions: true,
children: exampleCardViewContent
}
@ -315,6 +321,7 @@ const mockPosts = () => {
const examplePostsContent = (
<ViewContainer
actions={exampleActionButtons}
firstOnPage={false}
primaryAction={{
title: 'New post',
icon: 'add'
@ -329,25 +336,19 @@ const examplePostsContent = (
);
export const ExampleAlternativeList: Story = {
parameters: {
layout: 'fullscreen'
},
name: 'Example: Alternative List',
args: {
pageTabs: pageTabs,
showPageMenu: true,
showAppMenu: true,
showGlobalActions: true,
children: examplePostsContent
}
};
export const ExampleDetailScreen: Story = {
parameters: {
layout: 'fullscreen'
},
name: 'Example: Detail Page',
args: {
showPageMenu: true,
showAppMenu: true,
breadCrumbs: <Breadcrumbs
items={[
{
@ -362,19 +363,23 @@ export const ExampleDetailScreen: Story = {
showGlobalActions: true,
children: <>
<ViewContainer
toolbarBorder={false}
type='page'>
<div className='flex items-end justify-between gap-5 border-b border-grey-200 py-2'>
firstOnPage={false}
headerContent={
<div>
<Avatar bgColor='#A5D5F7' label='EV' labelColor='white' size='xl' />
<Heading className='mt-2' level={1}>Emerson Vaccaro</Heading>
<div className=''>Colombus, OH</div>
</div>
<div className='pb-2'>
<Button color='outline' icon='ellipsis' />
</div>
</div>
<div className='grid grid-cols-4 border-b border-grey-200 py-5'>
}
primaryAction={
{
icon: 'ellipsis',
color: 'outline'
}
}
type='page'
>
<div className='grid grid-cols-4 border-b border-grey-200 pb-5'>
<div className='-ml-5 flex h-full flex-col px-5'>
<span>Last seen on <strong>22 June 2023</strong></span>
<span className='mt-2'>Created on <strong>27 Jan 2021</strong></span>

View File

@ -1,10 +1,11 @@
import React from 'react';
import {TabList} from '../TabView';
import clsx from 'clsx';
import PageMenu from './PageMenu';
import AppMenu from './AppMenu';
import GlobalActions from './GlobalActions';
import Button from '../Button';
import {BreadcrumbsProps} from '../Breadcrumbs';
import PageHeader from './PageHeader';
export interface PageTab {
id: string;
@ -16,21 +17,69 @@ export interface CustomGlobalAction {
onClick?: () => void;
}
interface PageToolbarProps {
interface PageProps {
mainContainerClassName?: string;
mainClassName?: string;
showPageMenu?: boolean;
fullBleedPage?: boolean;
/**
* The pageToolbar is a WIP part of this component, it's unused ATM in Ghost Admin.
*/
pageToolbarClassName?: string;
fullBleedToolbar?: boolean;
/**
* TK. Part of the Page Toolbar
*/
showAppMenu?: boolean;
/**
* Show
*/
showGlobalActions?: boolean;
/**
* TK. Part of the Page Toolbar
*/
customGlobalActions?: CustomGlobalAction[];
breadCrumbs?: React.ReactElement<BreadcrumbsProps>;
/**
* TK. Part of the Page Toolbar
*/
pageTabs?: PageTab[],
/**
* TK. Part of the Page Toolbar
*/
selectedTab?: string;
/**
* TK. Part of the Page Toolbar
*/
onTabChange?: (id: string) => void;
children?: React.ReactNode;
}
const PageToolbar: React.FC<PageToolbarProps> = ({
/**
* The page component is the main container in Ghost Admin. It consists of a
* page level toolbar (`pageToolbar`  unused ATM, it's for page level views and
* navigation in the future), and the main content area.
*
* ### Examples
* You can find several examples in the sidebar. If you're building a page for the
* current Admin you can use the ["List in Current Admin"](/story/global-layout-page--example-current-admin-list)
* example as a starting point. The rest of the examples are showing a potential direction for a
* future structure.
*/
const Page: React.FC<PageProps> = ({
fullBleedPage = true,
mainContainerClassName,
mainClassName,
showPageMenu = false,
pageToolbarClassName,
fullBleedToolbar = true,
showAppMenu = false,
showGlobalActions = false,
customGlobalActions,
breadCrumbs,
@ -48,9 +97,10 @@ const PageToolbar: React.FC<PageToolbarProps> = ({
selectedTab = pageTabs[0].id;
}
const left: React.ReactNode = <div className='flex items-center gap-10'>
{showPageMenu && (
<PageMenu />
const left: React.ReactNode = (
(showAppMenu || breadCrumbs || pageTabs?.length) && <div className='flex items-center gap-10'>
{showAppMenu && (
<AppMenu />
)}
{breadCrumbs}
{pageTabs?.length && (
@ -63,15 +113,15 @@ const PageToolbar: React.FC<PageToolbarProps> = ({
width='normal'
/>
)}
</div>;
</div>);
mainClassName = clsx(
'flex h-[calc(100%-72px)] w-[100vw] flex-auto flex-col',
'flex w-full flex-auto flex-col',
mainClassName
);
const globalActions = (
(customGlobalActions?.length || showGlobalActions) &&
<div className='sticky flex items-center gap-7'>
{(customGlobalActions?.map((action) => {
return (
@ -79,22 +129,34 @@ const PageToolbar: React.FC<PageToolbarProps> = ({
);
}))}
{showGlobalActions && <GlobalActions />}
</div>
</div>);
mainContainerClassName = clsx(
'flex h-[100vh] w-full flex-col overflow-y-auto overflow-x-hidden',
!fullBleedPage && 'mx-auto max-w-7xl',
mainContainerClassName
);
pageToolbarClassName = clsx(
'sticky top-0 z-50 flex h-18 w-full items-center justify-between gap-5 bg-white p-6',
!fullBleedToolbar && 'mx-auto max-w-7xl',
pageToolbarClassName
);
return (
<div className='w-100 h-[100vh] overflow-y-auto overflow-x-hidden'>
<header className='sticky top-0 z-50 flex h-18 items-center justify-between gap-5 bg-white p-6'>
<nav>{left}</nav>
<div>{globalActions}</div>
</header>
<div className={mainContainerClassName}>
{(left || globalActions) &&
<PageHeader
containerClassName={pageToolbarClassName}
left={left}
right={globalActions}
/>
}
<main className={mainClassName}>
<section className='mx-auto flex h-full w-full flex-col'>
{children}
</section>
</main>
</div>
);
};
export default PageToolbar;
export default Page;

View File

@ -28,7 +28,7 @@ const PageHeader: React.FC<PageHeaderProps> = ({
children
}) => {
const containerClasses = clsx(
'z-50 h-[74px] p-5 px-7',
'z-50 h-[72px] p-5 px-7',
!children && 'flex items-center justify-between gap-3',
sticky && 'sticky top-0',
containerClassName

View File

@ -18,33 +18,34 @@ const meta = {
}}
/>;
},
tags: ['autodocs']
argTypes: {
children: {
control: {
type: 'text'
}
}
},
tags: ['autodocs'],
excludeStories: ['exampleActions']
} satisfies Meta<typeof ViewContainer>;
export default meta;
type Story = StoryObj<typeof ViewContainer>;
const ContentContainer: React.FC<{children: React.ReactNode}> = ({
children
}) => {
return <div className='m-auto max-w-[800px] p-5 text-center'>{children}</div>;
};
export const exampleActions = [
<Button label='Filter' link onClick={() => {
<Button label='Filter' onClick={() => {
alert('Clicked filter');
}} />,
<Button label='Sort' link onClick={() => {
<Button label='Sort' onClick={() => {
alert('Clicked sort');
}} />,
<Button icon='magnifying-glass' size='sm' link onClick={() => {
<Button icon='magnifying-glass' size='sm' onClick={() => {
alert('Clicked search');
}} />,
<ButtonGroup buttons={[
{
icon: 'listview',
size: 'sm',
link: true,
iconColorClass: 'text-black',
onClick: () => {
alert('Clicked list view');
@ -53,13 +54,12 @@ export const exampleActions = [
{
icon: 'cardview',
size: 'sm',
link: true,
iconColorClass: 'text-grey-500',
onClick: () => {
alert('Clicked card view');
}
}
]} />
]} clearBg={false} link />
];
const primaryAction: PrimaryActionProps = {
@ -74,12 +74,12 @@ const tabs: ViewTab[] = [
{
id: 'steph',
title: 'Steph Curry',
contents: <ContentContainer>The tabs component lets you add various datasets. It uses the <code>`TabList`</code> component to stay consistent with the simple TabView.</ContentContainer>
contents: 'The tabs component lets you add various datasets. It uses the <code>`TabList`</code> component to stay consistent with the simple TabView.'
},
{
id: 'klay',
title: 'Klay Thompson',
contents: <ContentContainer>Splash brother #11.</ContentContainer>
contents: 'Splash brother #11.'
}
];
@ -87,7 +87,7 @@ export const Default: Story = {
args: {
type: 'page',
toolbarBorder: false,
children: <ContentContainer>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.</ContentContainer>
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.'
}
};
@ -96,7 +96,7 @@ export const PageType: Story = {
args: {
type: 'page',
title: 'Page title',
children: <ContentContainer>In its simplest form you can use this component as the main container of pages.</ContentContainer>
children: 'In its simplest form you can use this component as the main container of pages.'
}
};
@ -105,7 +105,7 @@ export const SectionType: Story = {
args: {
type: 'section',
title: 'Section title',
children: <ContentContainer>This example shows how to use it for sections on a page.</ContentContainer>
children: 'This example shows how to use it for sections on a page.'
}
};
@ -113,7 +113,8 @@ export const PrimaryActionOnPage: Story = {
args: {
type: 'page',
title: 'Page title',
primaryAction: primaryAction
primaryAction: primaryAction,
children: 'View contents'
}
};

View File

@ -21,12 +21,50 @@ export interface PrimaryActionProps {
title?: string;
icon?: string;
color?: ButtonColor;
className?: string;
onClick?: () => void;
}
interface ViewContainerProps {
/**
* Use `page` if the `ViewContainer` is your main component on the page. Use
* `section` for individual sections on the page (e.g. blocks on a dashboard).
*/
type: 'page' | 'section';
/**
* The title of the ViewContainer. `page` type containers will use a large
* size that matches the rest of the page titles in the Admin.
*/
title?: string;
/**
* Use this if there's no toolbar on the page and you use the `ViewContainer`
* as the main container on a page. Technically it sticks the header to
* the top of the page with the actions aligned properly to match other
* pages in the Admin.
*/
firstOnPage?:boolean;
/**
* Use this for custom content in the header.
*/
headerContent?: React.ReactNode;
/**
* Sticks the header so it's always visible. The `top` value depends on the
* value of `firstOnPage`:
*
* ```
* firstOnPage = true -> top: 0px;
* firstOnPage = false -> top: 3vmin;
* ```
*/
stickyHeader?: boolean;
/**
* Use this to break down the view to multiple tabs.
*/
tabs?: ViewTab[];
selectedTab?: string;
selectedView?: string;
@ -36,18 +74,40 @@ interface ViewContainerProps {
toolbarContainerClassName?: string;
toolbarLeftClassName?: string;
toolbarBorder?: boolean;
/**
* The primary action appears in the view container's top right usually as a solid
* button.
*/
primaryAction?: PrimaryActionProps;
actions?: (React.ReactElement<ButtonProps> | React.ReactElement<ButtonGroupProps>)[];
/**
* Adds more actions by the primary action, primarily buttons and button groups.
*/
actions?: (React.ReactElement<ButtonProps> | React.ReactElement<ButtonGroupProps> | React.ReactNode)[];
actionsClassName?: string;
actionsHidden?: boolean;
contentWrapperClassName?: string;
/**
* Sets the width of the view container full bleed
*/
contentFullBleed?: boolean;
children?: React.ReactNode;
}
/**
* The `ViewContainer` component is a generic container for either the complete
* contents of a page (`type = 'page'`) or for individual sections on a
* page, like blocks on a dashboard (`type = 'section'`). It has a bunch of
* parameters to customise its look & feel.
*/
const ViewContainer: React.FC<ViewContainerProps> = ({
type,
title,
firstOnPage = true,
headerContent,
stickyHeader = true,
tabs,
selectedTab,
onTabChange,
@ -112,12 +172,14 @@ const ViewContainer: React.FC<ViewContainerProps> = ({
toolbarWrapperClassName = clsx(
'z-50',
type === 'page' && 'sticky top-18 mx-auto w-full max-w-7xl bg-white px-12 pt-[3vmin]',
type === 'page' && 'mx-auto w-full max-w-7xl bg-white px-12',
(type === 'page' && stickyHeader) && (firstOnPage ? 'sticky top-0 pt-8' : 'sticky top-18 pt-[3vmin]'),
toolbarContainerClassName
);
toolbarContainerClassName = clsx(
'flex justify-between',
'flex items-end justify-between',
(firstOnPage && type === 'page') ? 'pb-8' : (tabs?.length ? '' : 'pb-2'),
toolbarBorder && 'border-b border-grey-200',
toolbarContainerClassName
);
@ -128,27 +190,29 @@ const ViewContainer: React.FC<ViewContainerProps> = ({
);
actionsClassName = clsx(
'flex items-center gap-10 transition-all',
'flex items-center gap-5 transition-all',
actionsHidden && 'opacity-0 group-hover/view-container:opacity-100',
tabs?.length ? 'pb-2' : 'pb-3',
tabs?.length ? 'pb-2' : (type === 'page' ? 'pb-1' : ''),
actionsClassName
);
if (primaryAction) {
primaryAction!.color = 'black';
}
const primaryActionContents = <>
{primaryAction?.title && (
<Button color={primaryAction.color} icon={primaryAction.icon} iconColorClass='text-white' label={primaryAction.title} size={type === 'page' ? 'md' : 'sm'} onClick={primaryAction.onClick} />
{(primaryAction?.title || primaryAction?.icon) && (
<Button className={primaryAction.className} color={primaryAction.color || 'black'} icon={primaryAction.icon} label={primaryAction.title} size={type === 'page' ? 'md' : 'sm'} onClick={primaryAction.onClick} />
)}
</>;
const headingClassName = clsx(
tabs?.length && 'pb-3',
type === 'page' && '-mt-2'
);
toolbar = (
<div className={toolbarWrapperClassName}>
<div className={toolbarContainerClassName}>
<div className={toolbarLeftClassName}>
{title && <Heading className={tabs?.length ? 'pb-3' : 'pb-2'} level={type === 'page' ? 1 : 4}>{title}</Heading>}
{headerContent}
{title && <Heading className={headingClassName} level={type === 'page' ? 1 : 4}>{title}</Heading>}
{tabs?.length && (
<TabList
border={false}
@ -179,14 +243,14 @@ const ViewContainer: React.FC<ViewContainerProps> = ({
contentWrapperClassName = clsx(
'relative mx-auto w-full flex-auto',
!contentFullBleed && 'max-w-7xl px-12',
(!contentFullBleed && type === 'page') && 'max-w-7xl px-12',
contentWrapperClassName,
(!title && !actions) && 'pt-[3vmin]'
);
return (
<section className={mainContainerClassName}>
{(title || actions) && toolbar}
{(title || actions || headerContent) && toolbar}
<div className={contentWrapperClassName}>
{mainContent}
</div>

View File

@ -9,7 +9,8 @@ import Button from '../Button';
const meta = {
title: 'Global / Table / Dynamic Table',
component: DynamicTable,
tags: ['autodocs']
tags: ['autodocs'],
excludeStories: ['testColumns', 'testRows']
} satisfies Meta<typeof DynamicTable>;
export default meta;

View File

@ -81,7 +81,7 @@ const DynamicTable: React.FC<DynamicTableProps> = ({
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)]',
(singlePageTable && (stickyHeader || stickyFooter || absolute)) && 'px-12 xl:px-[calc((100%-1320px)/2+48px)]',
tableContainerClassName
);
@ -91,7 +91,7 @@ const DynamicTable: React.FC<DynamicTableProps> = ({
);
thClassName = clsx(
'bg-white py-3 pr-3 text-left',
'last-child:pr-5 bg-white py-3 text-left [&:not(:first-child)]:pl-5',
thClassName
);
@ -102,7 +102,7 @@ const DynamicTable: React.FC<DynamicTableProps> = ({
);
cellClassName = clsx(
'flex h-full py-3 pr-3',
'flex h-full py-4',
cellClassName
);
@ -114,8 +114,8 @@ const DynamicTable: React.FC<DynamicTableProps> = ({
footerClassName = clsx(
'bg-white',
(singlePageTable && stickyFooter) && 'mx-12 xl:mx-[calc((100%-1280px)/2+48px)]',
footer && 'py-3',
(singlePageTable && stickyFooter) && 'mx-12 xl:mx-[calc((100%-1320px)/2+48px)]',
footer && 'py-4',
stickyFooter && 'sticky inset-x-0 bottom-0',
footerBorder && 'border-t border-grey-200',
footerClassName
@ -166,7 +166,7 @@ const DynamicTable: React.FC<DynamicTableProps> = ({
let customTdClasses = tdClassName;
customTdClasses = clsx(
customTdClasses,
currentColumn.noWrap ? 'truncate' : '',
// currentColumn.noWrap ? 'truncate' : '',
currentColumn.align === 'center' && 'text-center',
currentColumn.align === 'right' && 'text-right'
);
@ -188,6 +188,9 @@ const DynamicTable: React.FC<DynamicTableProps> = ({
let customCellClasses = cellClassName;
customCellClasses = clsx(
customCellClasses,
colID !== 0 && 'pl-5',
(colID === columns.length - 1) && 'pr-5',
currentColumn.noWrap ? 'truncate' : '',
currentColumn.valign === 'middle' || !currentColumn.valign && 'items-center',
currentColumn.valign === 'top' && 'items-start',
currentColumn.valign === 'bottom' && 'items-end'

View File

@ -128,6 +128,17 @@ export {default as Tooltip} from './global/Tooltip';
export type {TooltipProps} from './global/Tooltip';
export {default as PageHeader} from './global/layout/PageHeader';
export type {PageHeaderProps} from './global/layout/PageHeader';
export {default as Page} from './global/layout/Page';
export type {PageTab} from './global/layout/Page';
export type {CustomGlobalAction} from './global/layout/Page';
export {default as ViewContainer} from './global/layout/ViewContainer';
export type {View} from './global/layout/ViewContainer';
export type {ViewTab} from './global/layout/ViewContainer';
export type {PrimaryActionProps} from './global/layout/ViewContainer';
export {default as DynamicTable} from './global/table/DynamicTable';
export type {DynamicTableProps} from './global/table/DynamicTable';
export type {DynamicTableColumn} from './global/table/DynamicTable';
export type {DynamicTableRow} from './global/table/DynamicTable';
export {default as SettingGroup} from './settings/SettingGroup';
export type {SettingGroupProps} from './settings/SettingGroup';
@ -159,4 +170,3 @@ export {confirmIfDirty} from './utils/modals';
export {default as DesignSystemApp} from './DesignSystemApp';
export type {DesignSystemAppProps} from './DesignSystemApp';
export {useFocusContext} from './providers/DesignSystemProvider';

View File

@ -12,7 +12,7 @@ module.exports = {
sm: '480px',
md: '640px',
lg: '1024px',
xl: '1280px',
xl: '1320px',
tablet: '860px'
},
colors: {
@ -247,7 +247,7 @@ module.exports = {
'4xl': '89.6rem',
'5xl': '102.4rem',
'6xl': '115.2rem',
'7xl': '128rem',
'7xl': '132rem',
'8xl': '140rem',
'9xl': '156rem',
full: '100%',

View File

@ -1444,11 +1444,11 @@
display: flex;
align-items: center;
overflow: hidden;
margin: -3px 0 0 0;
margin: -8px 0 0 0;
padding: 0;
text-overflow: ellipsis;
white-space: nowrap;
font-size: 3.2rem;
font-size: 3.6rem;
line-height: 1.3em;
font-weight: 700;
letter-spacing: -0.021em;