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 {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;

View File

@ -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>
); );
}); });

View File

@ -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;
}

View File

@ -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.
*/ */

View File

@ -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(

View File

@ -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'
}
}; };

View File

@ -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} />
))} ))}

View File

@ -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;

View File

@ -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>

View File

@ -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;

View File

@ -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

View File

@ -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'
} }
}; };

View File

@ -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>

View File

@ -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;

View File

@ -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'

View File

@ -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';

View File

@ -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%',

View File

@ -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;