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:
parent
8d0b9cd269
commit
6b46c828e7
@ -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 {useRouting} from '@tryghost/admin-x-framework/routing';
|
||||||
|
import {useState} from 'react';
|
||||||
|
|
||||||
const MainContent = () => {
|
const MainContent = () => {
|
||||||
const {updateRoute} = useRouting();
|
const {updateRoute} = useRouting();
|
||||||
|
const [view, setView] = useState<string>('list');
|
||||||
|
|
||||||
return <div>
|
const dummyActions = [
|
||||||
<Button label='Open modal' onClick={() => updateRoute('demo-modal')} />
|
<Button label='Filter' onClick={() => {
|
||||||
</div>;
|
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;
|
export default MainContent;
|
||||||
|
@ -1,18 +1,30 @@
|
|||||||
import NiceModal from '@ebay/nice-modal-react';
|
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';
|
import {useRouting} from '@tryghost/admin-x-framework/routing';
|
||||||
|
|
||||||
const DemoModal = NiceModal.create(() => {
|
const DemoModal = NiceModal.create(() => {
|
||||||
const {updateRoute} = useRouting();
|
const {updateRoute} = useRouting();
|
||||||
|
const modal = NiceModal.useModal();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Modal
|
<Modal
|
||||||
afterClose={() => {
|
afterClose={() => {
|
||||||
updateRoute('');
|
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>
|
</Modal>
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
@ -241,3 +241,7 @@ html, body, #root {
|
|||||||
grid-template-columns: auto 240px;
|
grid-template-columns: auto 240px;
|
||||||
gap: 32px;
|
gap: 32px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.sbdocs-a {
|
||||||
|
color: #30CF43 !important;
|
||||||
|
}
|
@ -150,6 +150,19 @@
|
|||||||
text-indent: 0; /* 1 */
|
text-indent: 0; /* 1 */
|
||||||
border-color: inherit; /* 2 */
|
border-color: inherit; /* 2 */
|
||||||
border-collapse: collapse; /* 3 */
|
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,
|
button,
|
||||||
[type='button'],
|
/* [type='button'], */
|
||||||
[type='reset'],
|
[type='reset'],
|
||||||
[type='submit'] {
|
[type='submit'] {
|
||||||
-webkit-appearance: button; /* 1 */
|
-webkit-appearance: button; /* 1 */
|
||||||
@ -201,6 +214,8 @@
|
|||||||
background-image: none; /* 2 */
|
background-image: none; /* 2 */
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
/*
|
/*
|
||||||
Use the modern Firefox focus style for all focusable elements.
|
Use the modern Firefox focus style for all focusable elements.
|
||||||
*/
|
*/
|
||||||
|
@ -32,7 +32,7 @@ const Button: React.FC<ButtonProps> = ({
|
|||||||
label = '',
|
label = '',
|
||||||
hideLabel = false,
|
hideLabel = false,
|
||||||
icon = '',
|
icon = '',
|
||||||
iconColorClass = 'text-black',
|
iconColorClass,
|
||||||
color = 'clear',
|
color = 'clear',
|
||||||
fullWidth,
|
fullWidth,
|
||||||
link,
|
link,
|
||||||
@ -67,6 +67,7 @@ const Button: React.FC<ButtonProps> = ({
|
|||||||
className
|
className
|
||||||
);
|
);
|
||||||
loadingIndicatorColor = 'light';
|
loadingIndicatorColor = 'light';
|
||||||
|
iconColorClass = iconColorClass || 'text-white';
|
||||||
break;
|
break;
|
||||||
case 'grey':
|
case 'grey':
|
||||||
className = clsx(
|
className = clsx(
|
||||||
@ -81,6 +82,7 @@ const Button: React.FC<ButtonProps> = ({
|
|||||||
className
|
className
|
||||||
);
|
);
|
||||||
loadingIndicatorColor = 'light';
|
loadingIndicatorColor = 'light';
|
||||||
|
iconColorClass = iconColorClass || 'text-white';
|
||||||
break;
|
break;
|
||||||
case 'red':
|
case 'red':
|
||||||
className = clsx(
|
className = clsx(
|
||||||
@ -88,6 +90,7 @@ const Button: React.FC<ButtonProps> = ({
|
|||||||
className
|
className
|
||||||
);
|
);
|
||||||
loadingIndicatorColor = 'light';
|
loadingIndicatorColor = 'light';
|
||||||
|
iconColorClass = iconColorClass || 'text-white';
|
||||||
break;
|
break;
|
||||||
case 'white':
|
case 'white':
|
||||||
className = clsx(
|
className = clsx(
|
||||||
|
@ -6,7 +6,8 @@ import {ButtonProps} from './Button';
|
|||||||
const ButtonGroupMeta = {
|
const ButtonGroupMeta = {
|
||||||
title: 'Global / Button Group',
|
title: 'Global / Button Group',
|
||||||
component: ButtonGroup,
|
component: ButtonGroup,
|
||||||
tags: ['autodocs']
|
tags: ['autodocs'],
|
||||||
|
decorators: [(_story: () => React.ReactNode) => (<div className='inline-block'>{_story()}</div>)]
|
||||||
} satisfies Meta<typeof ButtonGroup>;
|
} satisfies Meta<typeof ButtonGroup>;
|
||||||
|
|
||||||
export default ButtonGroupMeta;
|
export default ButtonGroupMeta;
|
||||||
@ -49,4 +50,21 @@ export const LinkButtons: Story = {
|
|||||||
buttons: linkButtons,
|
buttons: linkButtons,
|
||||||
link: true
|
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'
|
||||||
|
}
|
||||||
};
|
};
|
@ -1,18 +1,35 @@
|
|||||||
import React from 'react';
|
import React from 'react';
|
||||||
import Button from './Button';
|
import Button, {ButtonSize} from './Button';
|
||||||
|
|
||||||
import {ButtonProps} from './Button';
|
import {ButtonProps} from './Button';
|
||||||
|
import clsx from 'clsx';
|
||||||
|
|
||||||
export interface ButtonGroupProps {
|
export interface ButtonGroupProps {
|
||||||
|
size?: ButtonSize;
|
||||||
buttons: Array<ButtonProps>;
|
buttons: Array<ButtonProps>;
|
||||||
link?: boolean;
|
link?: boolean;
|
||||||
linkWithPadding?: boolean;
|
linkWithPadding?: boolean;
|
||||||
|
clearBg?: boolean;
|
||||||
className?: string;
|
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 (
|
return (
|
||||||
<div className={`flex items-center ${link ? 'gap-5' : 'gap-3'} ${className}`}>
|
<div className={groupColorClasses}>
|
||||||
{buttons.map(({key, ...props}) => (
|
{buttons.map(({key, ...props}) => (
|
||||||
<Button key={key} link={link} linkWithPadding={linkWithPadding} {...props} />
|
<Button key={key} link={link} linkWithPadding={linkWithPadding} {...props} />
|
||||||
))}
|
))}
|
||||||
|
@ -6,7 +6,12 @@ import Tooltip from './Tooltip';
|
|||||||
const meta = {
|
const meta = {
|
||||||
title: 'Global / Tooltip',
|
title: 'Global / Tooltip',
|
||||||
component: Tooltip,
|
component: Tooltip,
|
||||||
tags: ['autodocs']
|
tags: ['autodocs'],
|
||||||
|
decorators: [(_story: () => React.ReactNode) => (
|
||||||
|
<div className='p-10'>
|
||||||
|
{_story()}
|
||||||
|
</div>
|
||||||
|
)]
|
||||||
} satisfies Meta<typeof Tooltip>;
|
} satisfies Meta<typeof Tooltip>;
|
||||||
|
|
||||||
export default meta;
|
export default meta;
|
||||||
|
@ -20,6 +20,9 @@ const meta = {
|
|||||||
title: 'Global / Layout / Page',
|
title: 'Global / Layout / Page',
|
||||||
component: Page,
|
component: Page,
|
||||||
tags: ['autodocs'],
|
tags: ['autodocs'],
|
||||||
|
parameters: {
|
||||||
|
layout: 'fullscreen'
|
||||||
|
},
|
||||||
render: function Component(args) {
|
render: function Component(args) {
|
||||||
const [, updateArgs] = useArgs();
|
const [, updateArgs] = useArgs();
|
||||||
|
|
||||||
@ -35,7 +38,7 @@ const meta = {
|
|||||||
export default meta;
|
export default meta;
|
||||||
type Story = StoryObj<typeof Page>;
|
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[] = [
|
const customGlobalActions: CustomGlobalAction[] = [
|
||||||
{
|
{
|
||||||
@ -58,52 +61,66 @@ const pageTabs: Tab[] = [
|
|||||||
];
|
];
|
||||||
|
|
||||||
export const Default: Story = {
|
export const Default: Story = {
|
||||||
parameters: {
|
|
||||||
layout: 'fullscreen'
|
|
||||||
},
|
|
||||||
args: {
|
args: {
|
||||||
pageTabs: pageTabs,
|
pageTabs: pageTabs,
|
||||||
children: dummyContent
|
children: dummyContent
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
export const WithHamburger: Story = {
|
export const LimitToolbarWidth: Story = {
|
||||||
parameters: {
|
|
||||||
layout: 'fullscreen'
|
|
||||||
},
|
|
||||||
args: {
|
args: {
|
||||||
pageTabs: pageTabs,
|
pageTabs: pageTabs,
|
||||||
showPageMenu: true,
|
children: dummyContent,
|
||||||
|
fullBleedToolbar: false
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
export const WithHamburger: Story = {
|
||||||
|
args: {
|
||||||
|
pageTabs: pageTabs,
|
||||||
|
showAppMenu: true,
|
||||||
children: dummyContent
|
children: dummyContent
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
export const WithGlobalActions: Story = {
|
export const WithGlobalActions: Story = {
|
||||||
parameters: {
|
|
||||||
layout: 'fullscreen'
|
|
||||||
},
|
|
||||||
args: {
|
args: {
|
||||||
pageTabs: pageTabs,
|
pageTabs: pageTabs,
|
||||||
showPageMenu: true,
|
showAppMenu: true,
|
||||||
showGlobalActions: true,
|
showGlobalActions: true,
|
||||||
children: dummyContent
|
children: dummyContent
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
export const CustomGlobalActions: Story = {
|
export const CustomGlobalActions: Story = {
|
||||||
parameters: {
|
|
||||||
layout: 'fullscreen'
|
|
||||||
},
|
|
||||||
args: {
|
args: {
|
||||||
pageTabs: pageTabs,
|
pageTabs: pageTabs,
|
||||||
showPageMenu: true,
|
showAppMenu: true,
|
||||||
showGlobalActions: true,
|
showGlobalActions: true,
|
||||||
children: dummyContent,
|
children: dummyContent,
|
||||||
customGlobalActions: customGlobalActions
|
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
|
const simpleList = <ViewContainer
|
||||||
|
firstOnPage={false}
|
||||||
title='Members'
|
title='Members'
|
||||||
type='page'
|
type='page'
|
||||||
>
|
>
|
||||||
@ -115,19 +132,17 @@ const simpleList = <ViewContainer
|
|||||||
</ViewContainer>;
|
</ViewContainer>;
|
||||||
|
|
||||||
export const ExampleSimpleList: Story = {
|
export const ExampleSimpleList: Story = {
|
||||||
parameters: {
|
|
||||||
layout: 'fullscreen'
|
|
||||||
},
|
|
||||||
name: 'Example: Simple List',
|
name: 'Example: Simple List',
|
||||||
args: {
|
args: {
|
||||||
pageTabs: pageTabs,
|
pageTabs: pageTabs,
|
||||||
showPageMenu: true,
|
showAppMenu: true,
|
||||||
showGlobalActions: true,
|
showGlobalActions: true,
|
||||||
children: simpleList
|
children: simpleList
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const stickyList = <ViewContainer
|
const stickyList = <ViewContainer
|
||||||
|
firstOnPage={false}
|
||||||
title='Members'
|
title='Members'
|
||||||
type='page'
|
type='page'
|
||||||
>
|
>
|
||||||
@ -141,19 +156,17 @@ const stickyList = <ViewContainer
|
|||||||
</ViewContainer>;
|
</ViewContainer>;
|
||||||
|
|
||||||
export const ExampleStickyList: Story = {
|
export const ExampleStickyList: Story = {
|
||||||
parameters: {
|
|
||||||
layout: 'fullscreen'
|
|
||||||
},
|
|
||||||
name: 'Example: Sticky Header/Footer List',
|
name: 'Example: Sticky Header/Footer List',
|
||||||
args: {
|
args: {
|
||||||
pageTabs: pageTabs,
|
pageTabs: pageTabs,
|
||||||
showPageMenu: true,
|
showAppMenu: true,
|
||||||
showGlobalActions: true,
|
showGlobalActions: true,
|
||||||
children: stickyList
|
children: stickyList
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const examplePrimaryAction = <ViewContainer
|
const examplePrimaryAction = <ViewContainer
|
||||||
|
firstOnPage={false}
|
||||||
primaryAction={{
|
primaryAction={{
|
||||||
title: 'Add member',
|
title: 'Add member',
|
||||||
color: 'black',
|
color: 'black',
|
||||||
@ -174,13 +187,10 @@ const examplePrimaryAction = <ViewContainer
|
|||||||
</ViewContainer>;
|
</ViewContainer>;
|
||||||
|
|
||||||
export const ExamplePrimaryAction: Story = {
|
export const ExamplePrimaryAction: Story = {
|
||||||
parameters: {
|
|
||||||
layout: 'fullscreen'
|
|
||||||
},
|
|
||||||
name: 'Example: Primary Action',
|
name: 'Example: Primary Action',
|
||||||
args: {
|
args: {
|
||||||
pageTabs: pageTabs,
|
pageTabs: pageTabs,
|
||||||
showPageMenu: true,
|
showAppMenu: true,
|
||||||
showGlobalActions: true,
|
showGlobalActions: true,
|
||||||
children: examplePrimaryAction
|
children: examplePrimaryAction
|
||||||
}
|
}
|
||||||
@ -188,6 +198,7 @@ export const ExamplePrimaryAction: Story = {
|
|||||||
|
|
||||||
const exampleActionsContent = <ViewContainer
|
const exampleActionsContent = <ViewContainer
|
||||||
actions={exampleActionButtons}
|
actions={exampleActionButtons}
|
||||||
|
firstOnPage={false}
|
||||||
primaryAction={{
|
primaryAction={{
|
||||||
title: 'Add member',
|
title: 'Add member',
|
||||||
icon: 'add',
|
icon: 'add',
|
||||||
@ -209,13 +220,10 @@ const exampleActionsContent = <ViewContainer
|
|||||||
</ViewContainer>;
|
</ViewContainer>;
|
||||||
|
|
||||||
export const ExampleActions: Story = {
|
export const ExampleActions: Story = {
|
||||||
parameters: {
|
|
||||||
layout: 'fullscreen'
|
|
||||||
},
|
|
||||||
name: 'Example: Custom Actions',
|
name: 'Example: Custom Actions',
|
||||||
args: {
|
args: {
|
||||||
pageTabs: pageTabs,
|
pageTabs: pageTabs,
|
||||||
showPageMenu: true,
|
showAppMenu: true,
|
||||||
showGlobalActions: true,
|
showGlobalActions: true,
|
||||||
children: exampleActionsContent
|
children: exampleActionsContent
|
||||||
}
|
}
|
||||||
@ -246,6 +254,7 @@ const mockIdeaCards = () => {
|
|||||||
const exampleCardViewContent = (
|
const exampleCardViewContent = (
|
||||||
<ViewContainer
|
<ViewContainer
|
||||||
actions={exampleActionButtons}
|
actions={exampleActionButtons}
|
||||||
|
firstOnPage={false}
|
||||||
primaryAction={{
|
primaryAction={{
|
||||||
title: 'New idea',
|
title: 'New idea',
|
||||||
icon: 'add'
|
icon: 'add'
|
||||||
@ -260,13 +269,10 @@ const exampleCardViewContent = (
|
|||||||
);
|
);
|
||||||
|
|
||||||
export const ExampleCardView: Story = {
|
export const ExampleCardView: Story = {
|
||||||
parameters: {
|
|
||||||
layout: 'fullscreen'
|
|
||||||
},
|
|
||||||
name: 'Example: Card View',
|
name: 'Example: Card View',
|
||||||
args: {
|
args: {
|
||||||
pageTabs: pageTabs,
|
pageTabs: pageTabs,
|
||||||
showPageMenu: true,
|
showAppMenu: true,
|
||||||
showGlobalActions: true,
|
showGlobalActions: true,
|
||||||
children: exampleCardViewContent
|
children: exampleCardViewContent
|
||||||
}
|
}
|
||||||
@ -315,6 +321,7 @@ const mockPosts = () => {
|
|||||||
const examplePostsContent = (
|
const examplePostsContent = (
|
||||||
<ViewContainer
|
<ViewContainer
|
||||||
actions={exampleActionButtons}
|
actions={exampleActionButtons}
|
||||||
|
firstOnPage={false}
|
||||||
primaryAction={{
|
primaryAction={{
|
||||||
title: 'New post',
|
title: 'New post',
|
||||||
icon: 'add'
|
icon: 'add'
|
||||||
@ -329,25 +336,19 @@ const examplePostsContent = (
|
|||||||
);
|
);
|
||||||
|
|
||||||
export const ExampleAlternativeList: Story = {
|
export const ExampleAlternativeList: Story = {
|
||||||
parameters: {
|
|
||||||
layout: 'fullscreen'
|
|
||||||
},
|
|
||||||
name: 'Example: Alternative List',
|
name: 'Example: Alternative List',
|
||||||
args: {
|
args: {
|
||||||
pageTabs: pageTabs,
|
pageTabs: pageTabs,
|
||||||
showPageMenu: true,
|
showAppMenu: true,
|
||||||
showGlobalActions: true,
|
showGlobalActions: true,
|
||||||
children: examplePostsContent
|
children: examplePostsContent
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
export const ExampleDetailScreen: Story = {
|
export const ExampleDetailScreen: Story = {
|
||||||
parameters: {
|
|
||||||
layout: 'fullscreen'
|
|
||||||
},
|
|
||||||
name: 'Example: Detail Page',
|
name: 'Example: Detail Page',
|
||||||
args: {
|
args: {
|
||||||
showPageMenu: true,
|
showAppMenu: true,
|
||||||
breadCrumbs: <Breadcrumbs
|
breadCrumbs: <Breadcrumbs
|
||||||
items={[
|
items={[
|
||||||
{
|
{
|
||||||
@ -362,19 +363,23 @@ export const ExampleDetailScreen: Story = {
|
|||||||
showGlobalActions: true,
|
showGlobalActions: true,
|
||||||
children: <>
|
children: <>
|
||||||
<ViewContainer
|
<ViewContainer
|
||||||
toolbarBorder={false}
|
firstOnPage={false}
|
||||||
type='page'>
|
headerContent={
|
||||||
<div className='flex items-end justify-between gap-5 border-b border-grey-200 py-2'>
|
|
||||||
<div>
|
<div>
|
||||||
<Avatar bgColor='#A5D5F7' label='EV' labelColor='white' size='xl' />
|
<Avatar bgColor='#A5D5F7' label='EV' labelColor='white' size='xl' />
|
||||||
<Heading className='mt-2' level={1}>Emerson Vaccaro</Heading>
|
<Heading className='mt-2' level={1}>Emerson Vaccaro</Heading>
|
||||||
<div className=''>Colombus, OH</div>
|
<div className=''>Colombus, OH</div>
|
||||||
</div>
|
</div>
|
||||||
<div className='pb-2'>
|
}
|
||||||
<Button color='outline' icon='ellipsis' />
|
primaryAction={
|
||||||
</div>
|
{
|
||||||
</div>
|
icon: 'ellipsis',
|
||||||
<div className='grid grid-cols-4 border-b border-grey-200 py-5'>
|
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'>
|
<div className='-ml-5 flex h-full flex-col px-5'>
|
||||||
<span>Last seen on <strong>22 June 2023</strong></span>
|
<span>Last seen on <strong>22 June 2023</strong></span>
|
||||||
<span className='mt-2'>Created on <strong>27 Jan 2021</strong></span>
|
<span className='mt-2'>Created on <strong>27 Jan 2021</strong></span>
|
||||||
|
@ -1,10 +1,11 @@
|
|||||||
import React from 'react';
|
import React from 'react';
|
||||||
import {TabList} from '../TabView';
|
import {TabList} from '../TabView';
|
||||||
import clsx from 'clsx';
|
import clsx from 'clsx';
|
||||||
import PageMenu from './PageMenu';
|
import AppMenu from './AppMenu';
|
||||||
import GlobalActions from './GlobalActions';
|
import GlobalActions from './GlobalActions';
|
||||||
import Button from '../Button';
|
import Button from '../Button';
|
||||||
import {BreadcrumbsProps} from '../Breadcrumbs';
|
import {BreadcrumbsProps} from '../Breadcrumbs';
|
||||||
|
import PageHeader from './PageHeader';
|
||||||
|
|
||||||
export interface PageTab {
|
export interface PageTab {
|
||||||
id: string;
|
id: string;
|
||||||
@ -16,21 +17,69 @@ export interface CustomGlobalAction {
|
|||||||
onClick?: () => void;
|
onClick?: () => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface PageToolbarProps {
|
interface PageProps {
|
||||||
|
mainContainerClassName?: string;
|
||||||
mainClassName?: 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;
|
showGlobalActions?: boolean;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* TK. Part of the Page Toolbar
|
||||||
|
*/
|
||||||
customGlobalActions?: CustomGlobalAction[];
|
customGlobalActions?: CustomGlobalAction[];
|
||||||
breadCrumbs?: React.ReactElement<BreadcrumbsProps>;
|
breadCrumbs?: React.ReactElement<BreadcrumbsProps>;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* TK. Part of the Page Toolbar
|
||||||
|
*/
|
||||||
pageTabs?: PageTab[],
|
pageTabs?: PageTab[],
|
||||||
|
|
||||||
|
/**
|
||||||
|
* TK. Part of the Page Toolbar
|
||||||
|
*/
|
||||||
selectedTab?: string;
|
selectedTab?: string;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* TK. Part of the Page Toolbar
|
||||||
|
*/
|
||||||
onTabChange?: (id: string) => void;
|
onTabChange?: (id: string) => void;
|
||||||
|
|
||||||
children?: React.ReactNode;
|
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,
|
mainClassName,
|
||||||
showPageMenu = false,
|
pageToolbarClassName,
|
||||||
|
fullBleedToolbar = true,
|
||||||
|
showAppMenu = false,
|
||||||
showGlobalActions = false,
|
showGlobalActions = false,
|
||||||
customGlobalActions,
|
customGlobalActions,
|
||||||
breadCrumbs,
|
breadCrumbs,
|
||||||
@ -48,30 +97,31 @@ const PageToolbar: React.FC<PageToolbarProps> = ({
|
|||||||
selectedTab = pageTabs[0].id;
|
selectedTab = pageTabs[0].id;
|
||||||
}
|
}
|
||||||
|
|
||||||
const left: React.ReactNode = <div className='flex items-center gap-10'>
|
const left: React.ReactNode = (
|
||||||
{showPageMenu && (
|
(showAppMenu || breadCrumbs || pageTabs?.length) && <div className='flex items-center gap-10'>
|
||||||
<PageMenu />
|
{showAppMenu && (
|
||||||
)}
|
<AppMenu />
|
||||||
{breadCrumbs}
|
)}
|
||||||
{pageTabs?.length && (
|
{breadCrumbs}
|
||||||
<TabList
|
{pageTabs?.length && (
|
||||||
border={false}
|
<TabList
|
||||||
buttonBorder={false}
|
border={false}
|
||||||
handleTabChange={handleTabChange}
|
buttonBorder={false}
|
||||||
selectedTab={selectedTab}
|
handleTabChange={handleTabChange}
|
||||||
tabs={pageTabs!}
|
selectedTab={selectedTab}
|
||||||
width='normal'
|
tabs={pageTabs!}
|
||||||
/>
|
width='normal'
|
||||||
)}
|
/>
|
||||||
|
)}
|
||||||
</div>;
|
</div>);
|
||||||
|
|
||||||
mainClassName = clsx(
|
mainClassName = clsx(
|
||||||
'flex h-[calc(100%-72px)] w-[100vw] flex-auto flex-col',
|
'flex w-full flex-auto flex-col',
|
||||||
mainClassName
|
mainClassName
|
||||||
);
|
);
|
||||||
|
|
||||||
const globalActions = (
|
const globalActions = (
|
||||||
|
(customGlobalActions?.length || showGlobalActions) &&
|
||||||
<div className='sticky flex items-center gap-7'>
|
<div className='sticky flex items-center gap-7'>
|
||||||
{(customGlobalActions?.map((action) => {
|
{(customGlobalActions?.map((action) => {
|
||||||
return (
|
return (
|
||||||
@ -79,22 +129,34 @@ const PageToolbar: React.FC<PageToolbarProps> = ({
|
|||||||
);
|
);
|
||||||
}))}
|
}))}
|
||||||
{showGlobalActions && <GlobalActions />}
|
{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 (
|
return (
|
||||||
<div className='w-100 h-[100vh] overflow-y-auto overflow-x-hidden'>
|
<div className={mainContainerClassName}>
|
||||||
<header className='sticky top-0 z-50 flex h-18 items-center justify-between gap-5 bg-white p-6'>
|
{(left || globalActions) &&
|
||||||
<nav>{left}</nav>
|
<PageHeader
|
||||||
<div>{globalActions}</div>
|
containerClassName={pageToolbarClassName}
|
||||||
</header>
|
left={left}
|
||||||
|
right={globalActions}
|
||||||
|
/>
|
||||||
|
}
|
||||||
<main className={mainClassName}>
|
<main className={mainClassName}>
|
||||||
<section className='mx-auto flex h-full w-full flex-col'>
|
{children}
|
||||||
{children}
|
|
||||||
</section>
|
|
||||||
</main>
|
</main>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
export default PageToolbar;
|
export default Page;
|
@ -28,7 +28,7 @@ const PageHeader: React.FC<PageHeaderProps> = ({
|
|||||||
children
|
children
|
||||||
}) => {
|
}) => {
|
||||||
const containerClasses = clsx(
|
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',
|
!children && 'flex items-center justify-between gap-3',
|
||||||
sticky && 'sticky top-0',
|
sticky && 'sticky top-0',
|
||||||
containerClassName
|
containerClassName
|
||||||
|
@ -18,33 +18,34 @@ const meta = {
|
|||||||
}}
|
}}
|
||||||
/>;
|
/>;
|
||||||
},
|
},
|
||||||
tags: ['autodocs']
|
argTypes: {
|
||||||
|
children: {
|
||||||
|
control: {
|
||||||
|
type: 'text'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
tags: ['autodocs'],
|
||||||
|
excludeStories: ['exampleActions']
|
||||||
} satisfies Meta<typeof ViewContainer>;
|
} satisfies Meta<typeof ViewContainer>;
|
||||||
|
|
||||||
export default meta;
|
export default meta;
|
||||||
type Story = StoryObj<typeof ViewContainer>;
|
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 = [
|
export const exampleActions = [
|
||||||
<Button label='Filter' link onClick={() => {
|
<Button label='Filter' onClick={() => {
|
||||||
alert('Clicked filter');
|
alert('Clicked filter');
|
||||||
}} />,
|
}} />,
|
||||||
<Button label='Sort' link onClick={() => {
|
<Button label='Sort' onClick={() => {
|
||||||
alert('Clicked sort');
|
alert('Clicked sort');
|
||||||
}} />,
|
}} />,
|
||||||
<Button icon='magnifying-glass' size='sm' link onClick={() => {
|
<Button icon='magnifying-glass' size='sm' onClick={() => {
|
||||||
alert('Clicked search');
|
alert('Clicked search');
|
||||||
}} />,
|
}} />,
|
||||||
<ButtonGroup buttons={[
|
<ButtonGroup buttons={[
|
||||||
{
|
{
|
||||||
icon: 'listview',
|
icon: 'listview',
|
||||||
size: 'sm',
|
size: 'sm',
|
||||||
link: true,
|
|
||||||
iconColorClass: 'text-black',
|
iconColorClass: 'text-black',
|
||||||
onClick: () => {
|
onClick: () => {
|
||||||
alert('Clicked list view');
|
alert('Clicked list view');
|
||||||
@ -53,13 +54,12 @@ export const exampleActions = [
|
|||||||
{
|
{
|
||||||
icon: 'cardview',
|
icon: 'cardview',
|
||||||
size: 'sm',
|
size: 'sm',
|
||||||
link: true,
|
|
||||||
iconColorClass: 'text-grey-500',
|
iconColorClass: 'text-grey-500',
|
||||||
onClick: () => {
|
onClick: () => {
|
||||||
alert('Clicked card view');
|
alert('Clicked card view');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
]} />
|
]} clearBg={false} link />
|
||||||
];
|
];
|
||||||
|
|
||||||
const primaryAction: PrimaryActionProps = {
|
const primaryAction: PrimaryActionProps = {
|
||||||
@ -74,12 +74,12 @@ const tabs: ViewTab[] = [
|
|||||||
{
|
{
|
||||||
id: 'steph',
|
id: 'steph',
|
||||||
title: 'Steph Curry',
|
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',
|
id: 'klay',
|
||||||
title: 'Klay Thompson',
|
title: 'Klay Thompson',
|
||||||
contents: <ContentContainer>Splash brother #11.</ContentContainer>
|
contents: 'Splash brother #11.'
|
||||||
}
|
}
|
||||||
];
|
];
|
||||||
|
|
||||||
@ -87,7 +87,7 @@ export const Default: Story = {
|
|||||||
args: {
|
args: {
|
||||||
type: 'page',
|
type: 'page',
|
||||||
toolbarBorder: false,
|
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: {
|
args: {
|
||||||
type: 'page',
|
type: 'page',
|
||||||
title: 'Page title',
|
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: {
|
args: {
|
||||||
type: 'section',
|
type: 'section',
|
||||||
title: 'Section title',
|
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: {
|
args: {
|
||||||
type: 'page',
|
type: 'page',
|
||||||
title: 'Page title',
|
title: 'Page title',
|
||||||
primaryAction: primaryAction
|
primaryAction: primaryAction,
|
||||||
|
children: 'View contents'
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -21,12 +21,50 @@ export interface PrimaryActionProps {
|
|||||||
title?: string;
|
title?: string;
|
||||||
icon?: string;
|
icon?: string;
|
||||||
color?: ButtonColor;
|
color?: ButtonColor;
|
||||||
|
className?: string;
|
||||||
onClick?: () => void;
|
onClick?: () => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface ViewContainerProps {
|
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';
|
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;
|
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[];
|
tabs?: ViewTab[];
|
||||||
selectedTab?: string;
|
selectedTab?: string;
|
||||||
selectedView?: string;
|
selectedView?: string;
|
||||||
@ -36,18 +74,40 @@ interface ViewContainerProps {
|
|||||||
toolbarContainerClassName?: string;
|
toolbarContainerClassName?: string;
|
||||||
toolbarLeftClassName?: string;
|
toolbarLeftClassName?: string;
|
||||||
toolbarBorder?: boolean;
|
toolbarBorder?: boolean;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The primary action appears in the view container's top right usually as a solid
|
||||||
|
* button.
|
||||||
|
*/
|
||||||
primaryAction?: PrimaryActionProps;
|
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;
|
actionsClassName?: string;
|
||||||
actionsHidden?: boolean;
|
actionsHidden?: boolean;
|
||||||
contentWrapperClassName?: string;
|
contentWrapperClassName?: string;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sets the width of the view container full bleed
|
||||||
|
*/
|
||||||
contentFullBleed?: boolean;
|
contentFullBleed?: boolean;
|
||||||
children?: React.ReactNode;
|
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> = ({
|
const ViewContainer: React.FC<ViewContainerProps> = ({
|
||||||
type,
|
type,
|
||||||
title,
|
title,
|
||||||
|
firstOnPage = true,
|
||||||
|
headerContent,
|
||||||
|
stickyHeader = true,
|
||||||
tabs,
|
tabs,
|
||||||
selectedTab,
|
selectedTab,
|
||||||
onTabChange,
|
onTabChange,
|
||||||
@ -112,12 +172,14 @@ const ViewContainer: React.FC<ViewContainerProps> = ({
|
|||||||
|
|
||||||
toolbarWrapperClassName = clsx(
|
toolbarWrapperClassName = clsx(
|
||||||
'z-50',
|
'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
|
||||||
);
|
);
|
||||||
|
|
||||||
toolbarContainerClassName = clsx(
|
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',
|
toolbarBorder && 'border-b border-grey-200',
|
||||||
toolbarContainerClassName
|
toolbarContainerClassName
|
||||||
);
|
);
|
||||||
@ -128,27 +190,29 @@ const ViewContainer: React.FC<ViewContainerProps> = ({
|
|||||||
);
|
);
|
||||||
|
|
||||||
actionsClassName = clsx(
|
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',
|
actionsHidden && 'opacity-0 group-hover/view-container:opacity-100',
|
||||||
tabs?.length ? 'pb-2' : 'pb-3',
|
tabs?.length ? 'pb-2' : (type === 'page' ? 'pb-1' : ''),
|
||||||
actionsClassName
|
actionsClassName
|
||||||
);
|
);
|
||||||
|
|
||||||
if (primaryAction) {
|
|
||||||
primaryAction!.color = 'black';
|
|
||||||
}
|
|
||||||
|
|
||||||
const primaryActionContents = <>
|
const primaryActionContents = <>
|
||||||
{primaryAction?.title && (
|
{(primaryAction?.title || primaryAction?.icon) && (
|
||||||
<Button color={primaryAction.color} icon={primaryAction.icon} iconColorClass='text-white' label={primaryAction.title} size={type === 'page' ? 'md' : 'sm'} onClick={primaryAction.onClick} />
|
<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 = (
|
toolbar = (
|
||||||
<div className={toolbarWrapperClassName}>
|
<div className={toolbarWrapperClassName}>
|
||||||
<div className={toolbarContainerClassName}>
|
<div className={toolbarContainerClassName}>
|
||||||
<div className={toolbarLeftClassName}>
|
<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 && (
|
{tabs?.length && (
|
||||||
<TabList
|
<TabList
|
||||||
border={false}
|
border={false}
|
||||||
@ -179,14 +243,14 @@ const ViewContainer: React.FC<ViewContainerProps> = ({
|
|||||||
|
|
||||||
contentWrapperClassName = clsx(
|
contentWrapperClassName = clsx(
|
||||||
'relative mx-auto w-full flex-auto',
|
'relative mx-auto w-full flex-auto',
|
||||||
!contentFullBleed && 'max-w-7xl px-12',
|
(!contentFullBleed && type === 'page') && 'max-w-7xl px-12',
|
||||||
contentWrapperClassName,
|
contentWrapperClassName,
|
||||||
(!title && !actions) && 'pt-[3vmin]'
|
(!title && !actions) && 'pt-[3vmin]'
|
||||||
);
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<section className={mainContainerClassName}>
|
<section className={mainContainerClassName}>
|
||||||
{(title || actions) && toolbar}
|
{(title || actions || headerContent) && toolbar}
|
||||||
<div className={contentWrapperClassName}>
|
<div className={contentWrapperClassName}>
|
||||||
{mainContent}
|
{mainContent}
|
||||||
</div>
|
</div>
|
||||||
|
@ -9,7 +9,8 @@ import Button from '../Button';
|
|||||||
const meta = {
|
const meta = {
|
||||||
title: 'Global / Table / Dynamic Table',
|
title: 'Global / Table / Dynamic Table',
|
||||||
component: DynamicTable,
|
component: DynamicTable,
|
||||||
tags: ['autodocs']
|
tags: ['autodocs'],
|
||||||
|
excludeStories: ['testColumns', 'testRows']
|
||||||
} satisfies Meta<typeof DynamicTable>;
|
} satisfies Meta<typeof DynamicTable>;
|
||||||
|
|
||||||
export default meta;
|
export default meta;
|
||||||
|
@ -81,7 +81,7 @@ const DynamicTable: React.FC<DynamicTableProps> = ({
|
|||||||
tableContainerClassName = clsx(
|
tableContainerClassName = clsx(
|
||||||
'flex-auto overflow-x-auto',
|
'flex-auto overflow-x-auto',
|
||||||
!horizontalScrolling && 'w-full max-w-full',
|
!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
|
tableContainerClassName
|
||||||
);
|
);
|
||||||
|
|
||||||
@ -91,7 +91,7 @@ const DynamicTable: React.FC<DynamicTableProps> = ({
|
|||||||
);
|
);
|
||||||
|
|
||||||
thClassName = clsx(
|
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
|
thClassName
|
||||||
);
|
);
|
||||||
|
|
||||||
@ -102,7 +102,7 @@ const DynamicTable: React.FC<DynamicTableProps> = ({
|
|||||||
);
|
);
|
||||||
|
|
||||||
cellClassName = clsx(
|
cellClassName = clsx(
|
||||||
'flex h-full py-3 pr-3',
|
'flex h-full py-4',
|
||||||
cellClassName
|
cellClassName
|
||||||
);
|
);
|
||||||
|
|
||||||
@ -114,8 +114,8 @@ const DynamicTable: React.FC<DynamicTableProps> = ({
|
|||||||
|
|
||||||
footerClassName = clsx(
|
footerClassName = clsx(
|
||||||
'bg-white',
|
'bg-white',
|
||||||
(singlePageTable && stickyFooter) && 'mx-12 xl:mx-[calc((100%-1280px)/2+48px)]',
|
(singlePageTable && stickyFooter) && 'mx-12 xl:mx-[calc((100%-1320px)/2+48px)]',
|
||||||
footer && 'py-3',
|
footer && 'py-4',
|
||||||
stickyFooter && 'sticky inset-x-0 bottom-0',
|
stickyFooter && 'sticky inset-x-0 bottom-0',
|
||||||
footerBorder && 'border-t border-grey-200',
|
footerBorder && 'border-t border-grey-200',
|
||||||
footerClassName
|
footerClassName
|
||||||
@ -166,7 +166,7 @@ const DynamicTable: React.FC<DynamicTableProps> = ({
|
|||||||
let customTdClasses = tdClassName;
|
let customTdClasses = tdClassName;
|
||||||
customTdClasses = clsx(
|
customTdClasses = clsx(
|
||||||
customTdClasses,
|
customTdClasses,
|
||||||
currentColumn.noWrap ? 'truncate' : '',
|
// currentColumn.noWrap ? 'truncate' : '',
|
||||||
currentColumn.align === 'center' && 'text-center',
|
currentColumn.align === 'center' && 'text-center',
|
||||||
currentColumn.align === 'right' && 'text-right'
|
currentColumn.align === 'right' && 'text-right'
|
||||||
);
|
);
|
||||||
@ -188,6 +188,9 @@ const DynamicTable: React.FC<DynamicTableProps> = ({
|
|||||||
let customCellClasses = cellClassName;
|
let customCellClasses = cellClassName;
|
||||||
customCellClasses = clsx(
|
customCellClasses = clsx(
|
||||||
customCellClasses,
|
customCellClasses,
|
||||||
|
colID !== 0 && 'pl-5',
|
||||||
|
(colID === columns.length - 1) && 'pr-5',
|
||||||
|
currentColumn.noWrap ? 'truncate' : '',
|
||||||
currentColumn.valign === 'middle' || !currentColumn.valign && 'items-center',
|
currentColumn.valign === 'middle' || !currentColumn.valign && 'items-center',
|
||||||
currentColumn.valign === 'top' && 'items-start',
|
currentColumn.valign === 'top' && 'items-start',
|
||||||
currentColumn.valign === 'bottom' && 'items-end'
|
currentColumn.valign === 'bottom' && 'items-end'
|
||||||
|
@ -128,6 +128,17 @@ export {default as Tooltip} from './global/Tooltip';
|
|||||||
export type {TooltipProps} from './global/Tooltip';
|
export type {TooltipProps} from './global/Tooltip';
|
||||||
export {default as PageHeader} from './global/layout/PageHeader';
|
export {default as PageHeader} from './global/layout/PageHeader';
|
||||||
export type {PageHeaderProps} 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 {default as SettingGroup} from './settings/SettingGroup';
|
||||||
export type {SettingGroupProps} from './settings/SettingGroup';
|
export type {SettingGroupProps} from './settings/SettingGroup';
|
||||||
@ -158,5 +169,4 @@ export {confirmIfDirty} from './utils/modals';
|
|||||||
|
|
||||||
export {default as DesignSystemApp} from './DesignSystemApp';
|
export {default as DesignSystemApp} from './DesignSystemApp';
|
||||||
export type {DesignSystemAppProps} from './DesignSystemApp';
|
export type {DesignSystemAppProps} from './DesignSystemApp';
|
||||||
export {useFocusContext} from './providers/DesignSystemProvider';
|
export {useFocusContext} from './providers/DesignSystemProvider';
|
||||||
|
|
@ -12,7 +12,7 @@ module.exports = {
|
|||||||
sm: '480px',
|
sm: '480px',
|
||||||
md: '640px',
|
md: '640px',
|
||||||
lg: '1024px',
|
lg: '1024px',
|
||||||
xl: '1280px',
|
xl: '1320px',
|
||||||
tablet: '860px'
|
tablet: '860px'
|
||||||
},
|
},
|
||||||
colors: {
|
colors: {
|
||||||
@ -247,7 +247,7 @@ module.exports = {
|
|||||||
'4xl': '89.6rem',
|
'4xl': '89.6rem',
|
||||||
'5xl': '102.4rem',
|
'5xl': '102.4rem',
|
||||||
'6xl': '115.2rem',
|
'6xl': '115.2rem',
|
||||||
'7xl': '128rem',
|
'7xl': '132rem',
|
||||||
'8xl': '140rem',
|
'8xl': '140rem',
|
||||||
'9xl': '156rem',
|
'9xl': '156rem',
|
||||||
full: '100%',
|
full: '100%',
|
||||||
|
@ -1444,11 +1444,11 @@
|
|||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
margin: -3px 0 0 0;
|
margin: -8px 0 0 0;
|
||||||
padding: 0;
|
padding: 0;
|
||||||
text-overflow: ellipsis;
|
text-overflow: ellipsis;
|
||||||
white-space: nowrap;
|
white-space: nowrap;
|
||||||
font-size: 3.2rem;
|
font-size: 3.6rem;
|
||||||
line-height: 1.3em;
|
line-height: 1.3em;
|
||||||
font-weight: 700;
|
font-weight: 700;
|
||||||
letter-spacing: -0.021em;
|
letter-spacing: -0.021em;
|
||||||
|
Loading…
Reference in New Issue
Block a user