@wix/design-system
Version:
@wix/design-system
843 lines (779 loc) • 26.2 kB
Markdown
## Feature Examples
### Structure
- description: Composer header consists of 3 areas:<br/>
 - Back button on the left.<br/>
 - Additional actions container in the center.<br/>
 - Main actions container on the right.<br/>
<br/>
There can be multiple containers with Additional actions in the middle. Available space will be evenly divided among them.<br/>
<br/>
Note that all areas are optional.
- example:
```jsx
<StorybookComponents.Stack flexDirection="column">
<ComposerHeader backButtonValue="Back button">
<ComposerHeader.Actions>
<StorybookComponents.Placeholder height="35px">
Additional actions
</StorybookComponents.Placeholder>
</ComposerHeader.Actions>
<ComposerHeader.MainActions>
<StorybookComponents.Placeholder height="35px">
Main actions
</StorybookComponents.Placeholder>
</ComposerHeader.MainActions>
</ComposerHeader>
<ComposerHeader backButtonValue="Back button">
<ComposerHeader.Actions justifyContent="flex-start">
<StorybookComponents.Placeholder height="35px">
Additional actions (1)
</StorybookComponents.Placeholder>
</ComposerHeader.Actions>
<ComposerHeader.Actions justifyContent="flex-end">
<StorybookComponents.Placeholder height="35px">
Additional actions (2)
</StorybookComponents.Placeholder>
</ComposerHeader.Actions>
<ComposerHeader.MainActions>
<StorybookComponents.Placeholder height="35px">
Main actions
</StorybookComponents.Placeholder>
</ComposerHeader.MainActions>
</ComposerHeader>
</StorybookComponents.Stack>
```
### Size
- description: Control component height and back button size with `size` prop:<br/>
 - `medium` (default) - use it in all common cases. Height is fixed at 66 px.<br/>
 - `small` - use it to maximise space for the content area. Height is fixed at 54 px.<br/>
<br/>
Note that the size for child items in `<ComposerHeader.MainActions/>`, `<ComposerHeader.Actions/>`, and `<ComposerHeader.SaveStatus/>` containers will have to be adjusted manually.
- example:
```jsx
<StorybookComponents.Stack flexDirection='column'>
<ComposerHeader backButtonValue="Medium Size">
<ComposerHeader.Actions justifyContent="flex-end">
<ComposerHeader.SaveStatus saveStatusValue="Medium size..." />
</ComposerHeader.Actions>
<ComposerHeader.MainActions>
<Button skin="inverted">Medium</Button>
<Button>Medium</Button>
</ComposerHeader.MainActions>
</ComposerHeader>
<ComposerHeader size="small" backButtonValue="Small Size">
<ComposerHeader.Actions justifyContent="flex-end">
<ComposerHeader.SaveStatus size="small" saveStatusValue="Small size..."/>
</ComposerHeader.Actions>
<ComposerHeader.MainActions>
<Button size="small" skin="inverted">Small</Button>
<Button size="small">Small</Button>
</ComposerHeader.MainActions>
</ComposerHeader>
</StorybookComponents.Stack>
```
### Shadow
- description: Add a shadow with `dropShadow` prop. It will replace the default bottom divider.
- example:
```jsx
<ComposerHeader backButtonValue="Back to Posts" dropShadow>
<ComposerHeader.MainActions>
<Button skin="inverted">Preview</Button>
<Button>Publish</Button>
</ComposerHeader.MainActions>
</ComposerHeader>
```
### Back Button
- description: Add a back button by specifying its label with `backButtonValue` prop.<br/>
<br/>
Button is added to the left side of a Composer header. Its position cannot be changed.
- example:
```jsx
<ComposerHeader backButtonValue="Back to Posts" />
```
### Main Actions
- description: Add main actions as children items to `<ComposerHeader.MainActions>` container on the right side of the Composer header. Container position is fixed and cannot be changed.<br/>
<br/>
In all common cases, use <a href="https://wix-style-react.com/?path=/story/components-actions--button">`<Button/>`</a> and <a href="https://wix-style-react.com/?path=/story/components-actions--textbutton">`<TextButton/>`</a> components to display the actions.
- example:
```jsx
<ComposerHeader>
<ComposerHeader.MainActions>
<Button skin="inverted">Preview</Button>
<Button>Publish</Button>
</ComposerHeader.MainActions>
</ComposerHeader>
```
### Additional Actions
- description: <p>Add secondary actions or content to <code><ComposerHeader.Actions/></code> container in the middle of the Composer header.</p><p></p><p>Use justifyContent prop to distribute content inside the container. It accepts 3 values:</p><li><code>flex-start</code> (default) - aligns items to the left.</li><li><code>center</code> - centers items within the container.</li><li><code>flex-end</code> - aligns items to the right.</li><p></p><p>Vertical dividers will be added automatically to separate primary and secondary actions.</p>
- example:
```jsx
<StorybookComponents.Stack flexDirection="Column">
<ComposerHeader backButtonValue="Back to Posts">
<ComposerHeader.Actions justifyContent="flex-end">
<ToggleButton labelValue="Undo">
<Icons.Undo />
</ToggleButton>
<ToggleButton labelValue="Redo">
<Icons.Redo />
</ToggleButton>
</ComposerHeader.Actions>
<ComposerHeader.MainActions>
<Button skin="inverted">Preview</Button>
<Button>Publish</Button>
</ComposerHeader.MainActions>
</ComposerHeader>
</StorybookComponents.Stack>;
```
### Save Status
- description: Add a save status to the Composer header with a compound `<ComposerHeader.SaveStatus/>` component. Include it in the additional actions container, justified to the end.<br/>
<br/>
Set save status label with `saveStatusValue` prop.<br/>
<br/>
Change its status to error with `saveStatusError` prop.
- example:
```jsx
<StorybookComponents.Stack flexDirection="Column">
<ComposerHeader backButtonValue="Back to Posts">
<ComposerHeader.Actions justifyContent="flex-end">
<ComposerHeader.SaveStatus saveStatusValue="Saving..." />
</ComposerHeader.Actions>
<ComposerHeader.MainActions>
<Button skin="inverted">Preview</Button>
<Button>Publish</Button>
</ComposerHeader.MainActions>
</ComposerHeader>
<ComposerHeader backButtonValue="Back to Posts">
<ComposerHeader.Actions justifyContent="flex-end">
<ComposerHeader.SaveStatus
saveStatusValue="Draft not saved"
saveStatusError
/>
</ComposerHeader.Actions>
<ComposerHeader.MainActions>
<Button skin="inverted">Preview</Button>
<Button>Publish</Button>
</ComposerHeader.MainActions>
</ComposerHeader>
</StorybookComponents.Stack>
```
## Common Use Case Examples
### Composer
- description: Include a Composer header on top of the composer environment.
- example:
```jsx
() => {
const [selectedSidebar, setSelectedSidebar] = React.useState(0);
const [activeTab, setActiveTab] = React.useState(0);
const [toggleImage, setToggleImage] = React.useState(true);
const [togglePosts, setTogglePosts] = React.useState(false);
const [toggleCommenting, setToggleCommenting] = React.useState(false);
const [descriptionValue, setDescriptionValue] = React.useState('');
const [authorTags, setAuthorTags] = React.useState([]);
const [categories, setCategories] = React.useState([
{
id: 0,
label: 'Other posts',
isChecked: false,
},
{
id: 1,
label: 'Recipes',
isChecked: false,
},
{
id: 2,
label: 'Lifestyle',
isChecked: false,
},
]);
const [publishDate, setPublishDate] = React.useState();
const [themeTags, setThemeTags] = React.useState([
{ id: '1', label: 'Sport' },
{ id: '2', label: 'Routine' },
]);
const [author, setAuthor] = React.useState();
const items = [
{
id: 0,
label: 'Settings',
icon: <Icons.Settings />,
},
{
id: 1,
label: 'Categories',
icon: <Icons.Category />,
},
{
id: 2,
label: 'Tags',
icon: <Icons.Tag />,
},
];
const authorOptions = [
listItemSelectBuilder({
id: 0,
prefix: <Avatar size="size18" />,
title: 'Doe John',
label: 'Doe John',
}),
listItemSelectBuilder({
id: 1,
prefix: <Avatar size="size18" />,
title: 'John Doe',
label: 'John Doe',
}),
listItemActionBuilder({
title: 'New Author',
prefixIcon: <Icons.Add />,
}),
];
const handleSelectTag = (tag, previousTags, setTags) =>
setTags([...previousTags, { id: tag.id, label: tag.value }]);
const handleRemoveTag = (tag, previousTags, setTags) =>
setTags(previousTags.filter(currTag => currTag.id !== tag));
const handleSidebarClick = id => {
setSelectedSidebar(id);
};
const handleCloseSidePanel = () => {
setSelectedSidebar();
};
const renderSettingsPanel = () => (
<SidePanel width="300px" onCloseButtonClick={handleCloseSidePanel}>
<SidePanel.Header title="Post Settings">
<Tabs
items={[
{ id: 0, title: 'General' },
{ id: 1, title: 'Advanced' },
]}
activeId={activeTab}
type="uniformSide"
width="114px"
onClick={value => setActiveTab(value.id)}
/>
</SidePanel.Header>
<SidePanel.Content>
{activeTab === 0 && (
<Box direction="vertical" gap="24px">
<FormField label="Display cover image" labelPlacement="left">
<ToggleSwitch
width="100%"
checked={toggleImage}
onChange={() => setToggleImage(!toggleImage)}
/>
</FormField>
{toggleImage && (
<Cell>
<Box height="120px">
<AddItem theme="image" tooltipContent="Add Item" />
</Box>
</Cell>
)}
<FormField label="Publish date">
<DatePicker
width="100%"
placeholderText="Publish Now"
popoverProps={{ appendTo: 'scrollParent' }}
autoFocus={false}
value={publishDate}
onChange={date => setPublishDate(date)}
/>
</FormField>
<FormField label="Author">
<AutoComplete
placeholder="Select Author"
options={authorOptions}
selectedId={author}
popoverProps={{ appendTo: 'window' }}
value={author}
onSelect={option => setAuthor(option.label)}
onChange={event => setAuthor(event.target.value)}
/>
</FormField>
<FormField label="Set as featured post" labelPlacement="left">
<ToggleSwitch
width="100%"
checked={togglePosts}
onChange={() => setTogglePosts(!togglePosts)}
/>
</FormField>
</Box>
)}
{activeTab === 1 && (
<Box direction="vertical" gap="24px">
<FormField label="Description">
<InputArea
placeholder="Add a description..."
value={descriptionValue}
onChange={e => setDescriptionValue(e.target.value)}
maxLength={140}
hasCounter
/>
</FormField>
<FormField label="Related post">
<MultiSelect
tags={authorTags}
options={[
{ id: '1', value: 'British authors' },
{ id: '2', value: 'French authors' },
]}
customSuffix={
<Box>
<TextButton prefixIcon={<Icons.Add />}>
Add Posts
</TextButton>
</Box>
}
onSelect={tag =>
handleSelectTag(tag, authorTags, setAuthorTags)
}
onRemoveTag={tag =>
handleRemoveTag(tag, authorTags, setAuthorTags)
}
/>
</FormField>
<FormField label="Enable commenting" labelPlacement="left">
<ToggleSwitch
width="100%"
checked={toggleCommenting}
onChange={() => setToggleCommenting(!toggleCommenting)}
/>
</FormField>
</Box>
)}
</SidePanel.Content>
</SidePanel>
);
const renderTagPanel = () => (
<SidePanel width="300px" onCloseButtonClick={handleCloseSidePanel}>
<SidePanel.Header title="Tags" showDivider={false} />
<SidePanel.Content noPadding>
<Box
direction="vertical"
gap="24px"
width="252px"
padding="0 24px 24px 24px"
>
<Text>
Add tags that describe what your post is about. Readers can use them
to find other posts with similar content.
</Text>
<FormField label="Tags">
<MultiSelect
tags={themeTags}
options={[
{ id: '1', value: 'Sport' },
{ id: '2', value: 'Routine' },
]}
onSelect={tag => handleSelectTag(tag, themeTags, setThemeTags)}
onRemoveTag={tag => handleRemoveTag(tag, themeTags, setThemeTags)}
/>
</FormField>
</Box>
</SidePanel.Content>
</SidePanel>
);
const renderCheckboxes = () => {
return categories.map(item => {
const handleChecked = id => {
const updatedCategories = [...categories];
const index = categories.findIndex(c => c.id === id);
updatedCategories[index].isChecked = !updatedCategories[index]
.isChecked;
setCategories(updatedCategories);
};
return (
<Checkbox
checked={item.isChecked}
onChange={() => handleChecked(item.id)}
id={item.id}
>
{item.label}
</Checkbox>
);
});
};
const renderCategoryPanel = () => {
return (
<SidePanel width="300px" onCloseButtonClick={handleCloseSidePanel}>
<SidePanel.Header title="Categories" showDivider={false} />
<SidePanel.Content noPadding>
<Box
direction="vertical"
gap="24px"
width="252px"
padding="0 24px 24px 24px"
>
<Text>
Let readers browse your posts by topic, e.g. food, lifestyle &
travel.
</Text>
<FormField label="Assign a category">
<Box direction="vertical" gap="12px">
{renderCheckboxes()}
<TextButton prefixIcon={<Icons.Add />}>
Create category
</TextButton>
</Box>
</FormField>
</Box>
</SidePanel.Content>
</SidePanel>
);
};
return (
<Layout gap={0}>
<Cell>
<ComposerHeader backButtonValue="Back">
<ComposerHeader.Actions justifyContent="flex-end">
<ToggleButton labelValue="Undo">
<Icons.Undo />
</ToggleButton>
<ToggleButton labelValue="Redo">
<Icons.Redo />
</ToggleButton>
</ComposerHeader.Actions>
<ComposerHeader.MainActions>
<Button skin="inverted">Preview</Button>
<Button>Publish</Button>
</ComposerHeader.MainActions>
</ComposerHeader>
</Cell>
<Cell>
<Box direction="vertical">
<Box gap="0" height="600px">
<ComposerSidebar
labelPlacement="bottom"
items={items}
selectedId={selectedSidebar}
onClick={(_, { id }) => handleSidebarClick(id)}
/>
{selectedSidebar === 0 && renderSettingsPanel()}
{selectedSidebar === 1 && renderCategoryPanel()}
{selectedSidebar === 2 && renderTagPanel()}
<Box backgroundColor="D70" width="100%">
<Box width="100%" padding={4}>
<StorybookComponents.Placeholder>
<Box verticalAlign="middle" align="center" height="100%">
<Text>Composer content area</Text>
</Box>
</StorybookComponents.Placeholder>
</Box>
</Box>
</Box>
</Box>
</Cell>
</Layout>
);
};
```
### Multiple Steps
- description: Allow users to break the creation process into multiple steps by placing <a href="https://wix-style-react.com/?path=/story/components-navigation--stepper">`<Stepper/>`</a> inside the Composer header.<br/>
<br/>
Use `<ComposerSidebar.Actions/>` container to store it.
- example:
```jsx
() => {
const [activeStep, setActiveStep] = React.useState(0);
const [selectedSidebar, setSelectedSidebar] = React.useState(0);
const [selectedThumbnail, setSelectedThumbnail] = React.useState(0);
const items = [
{
id: 0,
label: 'Add',
icon: <Icons.Add />,
},
{
id: 1,
label: 'Background',
icon: <Icons.Background />,
},
];
const handleNextClick = () => {
setActiveStep(activeStep + 1);
};
const handleBackClick = () => {
setActiveStep(activeStep - 1);
};
const handleCloseSidePanel = () => setSelectedSidebar();
const renderPublishPage = () => {
return (
<Page>
<Page.Header title="Publish & Share on Social" />
<Page.Content>
<Layout>
<Cell span={6}>
<BrowserPreviewWidget
backgroundColor="D70"
skin="custom"
browserBarSize="size18"
>
<Image
width="450px"
height="200px"
src="example.jpg"
borderRadius={0}
/>
</BrowserPreviewWidget>
</Cell>
<Cell span={6}>
<Box paddingBottom="30px" paddingTop="24px">
<Text>
Once you publish a campaign online, you can share it on social
to reach more people. To send it as an email, go back and add
recipients.
<br />
<br />
<b>Note:</b> Publishing a campaign without sending it first
will count as one of your monthly campaigns.
</Text>
</Box>
<Box align="right" gap={1}>
<Button priority="secondary">Cancel</Button>
<Button>Publish</Button>
</Box>
</Cell>
</Layout>
</Page.Content>
</Page>
);
};
const renderRecipientsPage = () => {
const columns = [
{
title: 'Name',
render: row => row.name,
width: '40%',
},
{
title: 'Email',
render: row => row.email,
width: '30%',
},
{
title: 'Phone',
render: row => row.phone,
width: '20%',
},
{
render: row => (
<TableActionCell
popoverMenuProps={{ appendTo: 'window' }}
secondaryActions={[
{
icon: <Icons.Delete />,
text: 'Delete',
onClick: () => {},
},
]}
/>
),
width: '10%',
},
];
const records = [
{ name: 'Yuki Tsunoda', email: 'yukif1@mail.com', phone: '214-399-0638' },
{
name: 'Ben Simons',
email: 'ben.simons@mail.com',
phone: '269-267-2573',
},
{
name: 'Alex Halifax',
email: 'ahalifaxalex@mail.com',
phone: '317-900-9252',
},
{
name: 'Walter Jenning',
email: 'walterjen@mail.com',
phone: '440-263-0433',
},
];
return (
<Page>
<Page.Header
title="Add Recipients"
subtitle="Type your recipients below or select from your contacts, labels, or filters."
/>
<Page.Content>
<Layout>
<Cell>
<FormField label="To:">
<MultiSelect
popoverProps={{ appendTo: 'window' }}
tags={[{ id: '0', label: 'Alex Halifax' }]}
options={[]}
customSuffix={
<Box>
<TextButton prefixIcon={<Icons.Add />}>
Add Contacts
</TextButton>
</Box>
}
/>
</FormField>
</Cell>
<Cell>
<Card>
<Table data={records} columns={columns} showSelection>
<Page.Sticky>
<Card>
<TableToolbar>
<TableToolbar.ItemGroup position="start">
<TableToolbar.Item>
<TableToolbar.Title>Contacts</TableToolbar.Title>
</TableToolbar.Item>
</TableToolbar.ItemGroup>
</TableToolbar>
<Table.Titlebar />
</Card>
</Page.Sticky>
<Card>
<Table.Content titleBarVisible={false} />
</Card>
</Table>
</Card>
</Cell>
</Layout>
</Page.Content>
</Page>
);
};
const renderThumbnail = (title, id, isBackground) => (
<Thumbnail
selected={selectedThumbnail === id}
title={<Text size="medium">{title}</Text>}
image={<Image />}
onClick={() => setSelectedThumbnail(id)}
backgroundImage={isBackground ? <Image height="114px" /> : undefined}
height="114px"
width="114px"
/>
);
const renderCreateStepSidePanel = () => (
<SidePanel width="300px" onCloseButtonClick={handleCloseSidePanel}>
<SidePanel.Header title="Add" showDivider={false} />
<SidePanel.Content noPadding stretchVertically>
<Box
direction="vertical"
gap="24px"
width="252px"
padding="10px 24px 24px 24px"
>
<Box gap={4}>
{renderThumbnail('Text', 0)}
{renderThumbnail('Image', 1)}
</Box>
<Box gap={4}>
{renderThumbnail('Button', 2)}
{renderThumbnail('Divider', 3)}
</Box>
<Box gap={4}>
{renderThumbnail('Social', 4)}
{renderThumbnail('Columns', 5)}
</Box>
</Box>
</SidePanel.Content>
</SidePanel>
);
const renderBackgroundSidePanel = () => {
return (
<SidePanel width="306px" onCloseButtonClick={handleCloseSidePanel}>
<SidePanel.Header title="Change background" showDivider={false} />
<SidePanel.Content noPadding>
<Box
direction="vertical"
gap="24px"
width="252px"
padding="0 24px 24px 24px"
>
<FormField label="Background color">
<ColorInput
value="#8EF2D8"
popoverProps={{ appendTo: 'scrollParent' }}
/>
</FormField>
<FormField label="Patterns & Images" />
<Box gap={4}>
<Box width="50%" height="114px">
<AddItem tooltipContent="Add Item" theme="image" />
</Box>
{renderThumbnail('', 0, true)}
</Box>
<Box gap={4}>
{renderThumbnail('', 1, true)}
{renderThumbnail('', 2, true)}
</Box>
<Box gap={4}>
{renderThumbnail('', 3, true)}
{renderThumbnail('', 4, true)}
</Box>
</Box>
</SidePanel.Content>
</SidePanel>
);
};
const renderCreateStep = () => (
<>
<ComposerSidebar
labelPlacement="bottom"
items={items}
selectedId={selectedSidebar}
onClick={(_, { id }) => setSelectedSidebar(id)}
/>
{selectedSidebar === 0 && renderCreateStepSidePanel()}
{selectedSidebar === 1 && renderBackgroundSidePanel()}
<Box backgroundColor="D70" width="100%">
<Box width="100%" padding={4}>
<StorybookComponents.Placeholder>
<Box verticalAlign="middle" align="center" height="100%">
<Text>Composer content area</Text>
</Box>
</StorybookComponents.Placeholder>
</Box>
</Box>
</>
);
return (
<Layout gap={0}>
<Cell>
<ComposerHeader
backButtonValue={activeStep > 0 ? 'Back' : undefined}
onBackClick={handleBackClick}
>
<ComposerHeader.Actions justifyContent="center">
<Stepper
type="text"
activeStep={activeStep}
steps={[
{ text: 'Create' },
{ text: 'Add Recipients' },
{ text: 'Publish & Send' },
]}
onClick={step => setActiveStep(step)}
/>
</ComposerHeader.Actions>
{activeStep !== 2 && (
<ComposerHeader.MainActions>
<Button
onClick={handleNextClick}
suffixIcon={<Icons.ArrowRight />}
>
Next
</Button>
</ComposerHeader.MainActions>
)}
</ComposerHeader>
</Cell>
<Cell>
<Box direction="vertical">
<Box gap="0" height="800px">
{activeStep === 0 && renderCreateStep()}
{activeStep === 1 && renderRecipientsPage()}
{activeStep === 2 && renderPublishPage()}
</Box>
</Box>
</Cell>
</Layout>
);
};
```