Skip to content

Commit 5b0044a

Browse files
committed
feat: Add panel-layout component
1 parent 1bfc081 commit 5b0044a

File tree

34 files changed

+2081
-12
lines changed

34 files changed

+2081
-12
lines changed

build-tools/utils/pluralize.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -54,6 +54,7 @@ const pluralizationMap = {
5454
NavigableGroup: 'NavigableGroups',
5555
Pagination: 'Paginations',
5656
AppLayoutToolbar: 'AppLayoutToolbars',
57+
PanelLayout: 'PanelLayouts',
5758
PieChart: 'PieCharts',
5859
Popover: 'Popovers',
5960
ProgressBar: 'ProgressBars',

pages/app-layout/styles.scss

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -123,3 +123,10 @@
123123
.ai-panel-logo {
124124
font-weight: 600;
125125
}
126+
127+
.ai-artifact-panel {
128+
block-size: 100%;
129+
overflow: auto;
130+
background-color: awsui.$color-background-layout-main;
131+
border-inline-start: 1px solid awsui.$color-border-divider-default;
132+
}

pages/app-layout/utils/external-global-left-panel-widget.tsx

Lines changed: 65 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,25 +1,49 @@
11
// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
22
// SPDX-License-Identifier: Apache-2.0
3-
import React from 'react';
3+
import React, { useState } from 'react';
44

5-
import { Box, Button } from '~components';
5+
import { useContainerQuery } from '@cloudscape-design/component-toolkit';
6+
7+
import { Box, Button, PanelLayout } from '~components';
68
import { registerLeftDrawer, updateDrawer } from '~components/internal/plugins/widget';
79
import { mount, unmount } from '~mount';
810

911
import styles from '../styles.scss';
1012

13+
const DEFAULT_SIZE = 360;
14+
const CHAT_SIZE = 280;
15+
const MIN_CHAT_SIZE = 150;
16+
const MIN_ARTIFACT_SIZE = 360;
17+
1118
const AIDrawer = () => {
12-
return (
19+
const [hasArtifact, setHasArtifact] = useState(false);
20+
const [artifactLoaded, setArtifactLoaded] = useState(false);
21+
const [chatSize, setChatSize] = useState(CHAT_SIZE);
22+
const [_maxPanelSize, ref] = useContainerQuery(entry => entry.contentBoxWidth - MIN_ARTIFACT_SIZE);
23+
const maxPanelSize = _maxPanelSize ?? Number.MAX_SAFE_INTEGER;
24+
const constrainedChatSize = Math.min(chatSize, maxPanelSize);
25+
const collapsed = constrainedChatSize < MIN_CHAT_SIZE;
26+
27+
const chatContent = (
1328
<Box padding="m">
1429
<Box variant="h2" padding={{ bottom: 'm' }}>
1530
Chat demo
1631
</Box>
1732
<Button
1833
onClick={() => {
34+
setHasArtifact(true);
1935
updateDrawer({ type: 'expandDrawer', payload: { id: 'ai-panel' } });
36+
updateDrawer({
37+
type: 'updateDrawerConfig',
38+
payload: {
39+
id: 'ai-panel',
40+
defaultSize: DEFAULT_SIZE + CHAT_SIZE,
41+
minSize: DEFAULT_SIZE + CHAT_SIZE,
42+
} as any,
43+
});
2044
}}
2145
>
22-
expand programmatically
46+
Open artifact
2347
</Button>
2448
<Button
2549
onClick={() => {
@@ -37,17 +61,52 @@ const AIDrawer = () => {
3761
resize to window.innerWidth + 1000
3862
</Button>
3963
{new Array(100).fill(null).map((_, index) => (
40-
<div key={index}>Tela content</div>
64+
<div key={index}>Tela chat content</div>
4165
))}
4266
</Box>
4367
);
68+
const artifactContent = (
69+
<Box padding="m">
70+
<Box variant="h2" padding={{ bottom: 'm' }}>
71+
Artifact
72+
</Box>
73+
<Button
74+
onClick={() => {
75+
setHasArtifact(false);
76+
updateDrawer({ type: 'exitExpandedMode' });
77+
updateDrawer({
78+
type: 'updateDrawerConfig',
79+
payload: { id: 'ai-panel', defaultSize: DEFAULT_SIZE, minSize: DEFAULT_SIZE } as any,
80+
});
81+
}}
82+
>
83+
Close artifact panel
84+
</Button>
85+
<Button onClick={() => setArtifactLoaded(true)}>Load artifact details</Button>
86+
{artifactLoaded && new Array(100).fill(null).map((_, index) => <div key={index}>Tela content</div>)}
87+
</Box>
88+
);
89+
return (
90+
<div ref={ref} style={{ width: '100%', overflow: 'hidden' }}>
91+
<PanelLayout
92+
resizable={true}
93+
panelSize={constrainedChatSize}
94+
maxPanelSize={maxPanelSize}
95+
minPanelSize={MIN_CHAT_SIZE}
96+
onPanelResize={({ detail }) => setChatSize(detail.panelSize)}
97+
panelContent={chatContent}
98+
mainContent={<div className={styles['ai-artifact-panel']}>{artifactContent}</div>}
99+
display={hasArtifact ? (collapsed ? 'main-only' : 'all') : 'panel-only'}
100+
/>
101+
</div>
102+
);
44103
};
45104

46105
registerLeftDrawer({
47106
id: 'ai-panel',
48107
resizable: true,
49108
isExpandable: true,
50-
defaultSize: 420,
109+
defaultSize: DEFAULT_SIZE,
51110
preserveInactiveContent: true,
52111

53112
ariaLabels: {
Lines changed: 203 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,203 @@
1+
// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
2+
// SPDX-License-Identifier: Apache-2.0
3+
import React, { useContext, useState } from 'react';
4+
5+
import { useContainerQuery } from '@cloudscape-design/component-toolkit';
6+
7+
import { Box, Checkbox, Drawer, FormField, Header, Input, SegmentedControl, SpaceBetween } from '~components';
8+
import AppLayout from '~components/app-layout';
9+
import Button from '~components/button';
10+
import { I18nProvider } from '~components/i18n';
11+
import messages from '~components/i18n/messages/all.en';
12+
import PanelLayout, { PanelLayoutProps } from '~components/panel-layout';
13+
14+
import AppContext, { AppContextType } from '../app/app-context';
15+
import labels from '../app-layout/utils/labels';
16+
import ScreenshotArea from '../utils/screenshot-area';
17+
18+
interface PanelLayoutContentProps {
19+
longPanelContent: boolean;
20+
longMainContent: boolean;
21+
minPanelSize: number;
22+
maxPanelSize: number;
23+
minContentSize: number;
24+
display: PanelLayoutProps.Display;
25+
panelPosition: PanelLayoutProps.PanelPosition;
26+
}
27+
type PageContext = React.Context<AppContextType<Partial<PanelLayoutContentProps>>>;
28+
29+
const PanelLayoutContent = ({
30+
longPanelContent,
31+
longMainContent,
32+
minContentSize,
33+
minPanelSize,
34+
maxPanelSize,
35+
display,
36+
panelPosition,
37+
}: PanelLayoutContentProps) => {
38+
const [size, setSize] = useState(Math.max(200, minPanelSize));
39+
40+
const [_actualMaxPanelSize, ref] = useContainerQuery(
41+
entry => Math.min(entry.contentBoxWidth - minContentSize, maxPanelSize),
42+
[minContentSize, maxPanelSize]
43+
);
44+
const actualMaxPanelSize = _actualMaxPanelSize ?? maxPanelSize;
45+
const actualSize = Math.min(size, actualMaxPanelSize);
46+
47+
const collapsed = actualMaxPanelSize < minPanelSize;
48+
49+
return (
50+
<div ref={ref} style={{ height: '100%', overflow: 'hidden' }}>
51+
{collapsed ? (
52+
'Collapsed view'
53+
) : (
54+
<PanelLayout
55+
panelSize={actualSize}
56+
minPanelSize={minPanelSize}
57+
maxPanelSize={actualMaxPanelSize}
58+
resizable={true}
59+
onPanelResize={({ detail }) => setSize(detail.panelSize)}
60+
display={display}
61+
panelPosition={panelPosition}
62+
panelContent={
63+
<Box padding="m">
64+
<Header>Panel content</Header>
65+
{new Array(longPanelContent ? 20 : 1)
66+
.fill('Lorem ipsum dolor sit amet, consectetur adipiscing elit.')
67+
.map((t, i) => (
68+
<div key={i}>{t}</div>
69+
))}
70+
<Button>Button</Button>
71+
</Box>
72+
}
73+
mainContent={
74+
<Box padding="m">
75+
<Header>Main content</Header>
76+
{new Array(longMainContent ? 200 : 1)
77+
.fill('Lorem ipsum dolor sit amet, consectetur adipiscing elit.')
78+
.map((t, i) => (
79+
<div key={i}>{t}</div>
80+
))}
81+
<Button>button</Button>
82+
</Box>
83+
}
84+
/>
85+
)}
86+
</div>
87+
);
88+
};
89+
90+
export default function PanelLayoutPage() {
91+
const {
92+
urlParams: {
93+
longPanelContent = false,
94+
longMainContent = false,
95+
minPanelSize = 200,
96+
maxPanelSize = 600,
97+
minContentSize = 600,
98+
display = 'all',
99+
panelPosition = 'side-start',
100+
},
101+
setUrlParams,
102+
} = useContext(AppContext as PageContext);
103+
104+
return (
105+
<I18nProvider messages={[messages]} locale="en">
106+
<ScreenshotArea disableAnimations={true} gutters={false}>
107+
<AppLayout
108+
ariaLabels={labels}
109+
navigation={
110+
<Drawer header="Settings">
111+
<SpaceBetween direction="vertical" size="m">
112+
<Checkbox
113+
checked={longMainContent}
114+
onChange={({ detail }) => setUrlParams({ longMainContent: detail.checked })}
115+
>
116+
Long main content
117+
</Checkbox>
118+
<Checkbox
119+
checked={longPanelContent}
120+
onChange={({ detail }) => setUrlParams({ longPanelContent: detail.checked })}
121+
>
122+
Long panel content
123+
</Checkbox>
124+
<FormField label="Minimum panel size">
125+
<Input
126+
type="number"
127+
value={minPanelSize.toString()}
128+
onChange={({ detail }) => setUrlParams({ minPanelSize: detail.value ? parseInt(detail.value) : 0 })}
129+
/>
130+
</FormField>
131+
<FormField label="Maximum panel size">
132+
<Input
133+
type="number"
134+
value={maxPanelSize.toString()}
135+
onChange={({ detail }) =>
136+
setUrlParams({ maxPanelSize: detail.value ? parseInt(detail.value) : Number.MAX_SAFE_INTEGER })
137+
}
138+
/>
139+
</FormField>
140+
<FormField label="Minimum content size">
141+
<Input
142+
type="number"
143+
value={minContentSize.toString()}
144+
onChange={({ detail }) =>
145+
setUrlParams({ minContentSize: detail.value ? parseInt(detail.value) : 0 })
146+
}
147+
/>
148+
</FormField>
149+
<FormField label="Panel position">
150+
<SegmentedControl
151+
options={[
152+
{ id: 'side-start', text: 'side-start' },
153+
{ id: 'side-end', text: 'side-end' },
154+
]}
155+
selectedId={panelPosition}
156+
onChange={({ detail }) => setUrlParams({ panelPosition: detail.selectedId as any })}
157+
/>
158+
</FormField>
159+
<FormField label="Display">
160+
<SegmentedControl
161+
options={[
162+
{ id: 'all', text: 'all' },
163+
{ id: 'panel-only', text: 'panel-only' },
164+
{ id: 'main-only', text: 'main-only' },
165+
]}
166+
selectedId={display}
167+
onChange={({ detail }) => setUrlParams({ display: detail.selectedId as any })}
168+
/>
169+
</FormField>
170+
</SpaceBetween>
171+
</Drawer>
172+
}
173+
content={<Header variant="h1">Panel layout in drawer demo</Header>}
174+
drawers={[
175+
{
176+
id: 'panel',
177+
content: (
178+
<PanelLayoutContent
179+
longMainContent={longMainContent}
180+
longPanelContent={longPanelContent}
181+
minPanelSize={minPanelSize}
182+
maxPanelSize={maxPanelSize}
183+
minContentSize={minContentSize}
184+
display={display}
185+
panelPosition={panelPosition}
186+
/>
187+
),
188+
resizable: true,
189+
defaultSize: 1000,
190+
ariaLabels: {
191+
drawerName: 'Panel',
192+
triggerButton: 'Open panel',
193+
closeButton: 'Close panel',
194+
resizeHandle: 'Resize drawer',
195+
},
196+
trigger: { iconName: 'contact' },
197+
},
198+
]}
199+
/>
200+
</ScreenshotArea>
201+
</I18nProvider>
202+
);
203+
}

0 commit comments

Comments
 (0)