Skip to content

Commit e1df0e6

Browse files
committed
feat(Layout): add Flex and Grid sub-components with stories
1 parent 75a0dc2 commit e1df0e6

File tree

8 files changed

+230
-9
lines changed

8 files changed

+230
-9
lines changed

src/components/content/Layout/GridLayout.tsx

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,9 @@ import { Styles, tasty } from '../../../tasty';
1111
import { CubeLayoutProps, Layout } from './Layout';
1212
import { LayoutBlock } from './LayoutBlock';
1313
import { LayoutContent } from './LayoutContent';
14+
import { LayoutFlex } from './LayoutFlex';
1415
import { LayoutFooter } from './LayoutFooter';
16+
import { LayoutGrid } from './LayoutGrid';
1517
import { LayoutHeader } from './LayoutHeader';
1618
import { LayoutPanel } from './LayoutPanel';
1719
import { LayoutPanelHeader } from './LayoutPanelHeader';
@@ -68,6 +70,8 @@ interface GridLayoutComponent
6870
Footer: typeof LayoutFooter;
6971
Content: typeof LayoutContent;
7072
Block: typeof LayoutBlock;
73+
Flex: typeof LayoutFlex;
74+
Grid: typeof LayoutGrid;
7175
Panel: typeof LayoutPanel;
7276
PanelHeader: typeof LayoutPanelHeader;
7377
}
@@ -78,6 +82,8 @@ const GridLayout = Object.assign(_GridLayout, {
7882
Footer: LayoutFooter,
7983
Content: LayoutContent,
8084
Block: LayoutBlock,
85+
Flex: LayoutFlex,
86+
Grid: LayoutGrid,
8187
Panel: LayoutPanel,
8288
PanelHeader: LayoutPanelHeader,
8389
}) as GridLayoutComponent;

src/components/content/Layout/Layout.stories.tsx

Lines changed: 97 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -514,6 +514,103 @@ export const CompleteApplicationShell: Story = {
514514
},
515515
};
516516

517+
export const FlexSubComponent: Story = {
518+
render: () => (
519+
<Layout height="300px">
520+
<Layout.Header title="Layout.Flex Example" />
521+
<Layout.Flex
522+
flow="row"
523+
gap="1x"
524+
placeItems="center"
525+
placeContent="center"
526+
>
527+
<Card width="100px" height="80px">
528+
Item 1
529+
</Card>
530+
<Card width="100px" height="80px">
531+
Item 2
532+
</Card>
533+
<Card width="100px" height="80px">
534+
Item 3
535+
</Card>
536+
</Layout.Flex>
537+
</Layout>
538+
),
539+
};
540+
541+
export const FlexWithScrolling: Story = {
542+
render: () => (
543+
<Layout height="200px">
544+
<Layout.Header title="Scrollable Flex Container" />
545+
<Layout.Flex flow="row" gap="1x" placeItems="start">
546+
{Array.from({ length: 20 }, (_, i) => (
547+
<Card key={i} width="100px" height="100px" flexShrink={0}>
548+
Item {i + 1}
549+
</Card>
550+
))}
551+
</Layout.Flex>
552+
</Layout>
553+
),
554+
};
555+
556+
export const GridSubComponent: Story = {
557+
render: () => (
558+
<Layout height="400px">
559+
<Layout.Header title="Layout.Grid Example" />
560+
<Layout.Grid
561+
columns="repeat(3, 1fr)"
562+
rows="repeat(2, 1fr)"
563+
gap="1x"
564+
flexGrow={1}
565+
>
566+
<Card>Cell 1</Card>
567+
<Card>Cell 2</Card>
568+
<Card>Cell 3</Card>
569+
<Card>Cell 4</Card>
570+
<Card>Cell 5</Card>
571+
<Card>Cell 6</Card>
572+
</Layout.Grid>
573+
</Layout>
574+
),
575+
};
576+
577+
export const GridWithTemplate: Story = {
578+
render: () => (
579+
<Layout height="400px">
580+
<Layout.Header title="Grid with Template" />
581+
<Layout.Grid
582+
template={`
583+
"header header header" auto
584+
"sidebar main main" 1fr
585+
"footer footer footer" auto
586+
/ 200px 1fr 1fr
587+
`}
588+
gap="1x"
589+
>
590+
<Card gridArea="header">Header</Card>
591+
<Card gridArea="sidebar">Sidebar</Card>
592+
<Card gridArea="main">Main Content</Card>
593+
<Card gridArea="footer">Footer</Card>
594+
</Layout.Grid>
595+
</Layout>
596+
),
597+
};
598+
599+
export const GridWithScrolling: Story = {
600+
render: () => (
601+
<Layout height="300px">
602+
<Layout.Header title="Scrollable Grid" />
603+
<Layout.Grid gap="1x">
604+
{Array.from({ length: 16 }, (_, i) => (
605+
<Card key={i} width="100px" height="80px" whiteSpace="nowrap">
606+
Card {i + 1}
607+
</Card>
608+
))}
609+
</Layout.Grid>
610+
</Layout>
611+
),
612+
};
613+
517614
/**
518615
* Layout automatically applies height: 100% when it detects it has collapsed
519616
* to 0 height. This happens when the Layout is placed in a container without

src/components/content/Layout/LayoutContent.tsx

Lines changed: 13 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -3,10 +3,12 @@ import { useHover } from 'react-aria';
33

44
import {
55
BaseProps,
6-
CONTAINER_STYLES,
76
ContainerStyleProps,
87
extractStyles,
98
filterBaseProps,
9+
INNER_STYLES,
10+
mergeStyles,
11+
OUTER_STYLES,
1012
tasty,
1113
} from '../../../tasty';
1214
import { useCombinedRefs } from '../../../utils/react';
@@ -28,7 +30,7 @@ const ContentElement = tasty({
2830
flexGrow: 1,
2931
height: 'min 0',
3032
overflow: 'hidden',
31-
boxSizing: 'border-box',
33+
boxSizing: 'content-box',
3234

3335
Inner: {
3436
display: 'flex',
@@ -93,7 +95,8 @@ function LayoutContent(
9395
ref: ForwardedRef<HTMLDivElement>,
9496
) {
9597
const { children, scrollbar = 'thin', styles, ...otherProps } = props;
96-
const extractedStyles = extractStyles(otherProps, CONTAINER_STYLES);
98+
const outerStyles = extractStyles(otherProps, OUTER_STYLES);
99+
const innerStyles = extractStyles(otherProps, INNER_STYLES);
97100
const innerRef = useRef<HTMLDivElement>(null);
98101
const combinedRef = useCombinedRefs(ref);
99102
const isTinyScrollbar = scrollbar === 'tiny';
@@ -119,13 +122,14 @@ function LayoutContent(
119122
[scrollbar, isHovered],
120123
);
121124

122-
// Merge incoming styles with extracted container styles for Inner element
125+
// Merge styles: outer styles to root, inner styles to Inner element
123126
const finalStyles = useMemo(() => {
124-
return {
125-
...styles,
126-
Inner: { ...(styles?.Inner as object), ...extractedStyles },
127-
};
128-
}, [styles, extractedStyles]);
127+
return mergeStyles(
128+
styles,
129+
outerStyles,
130+
innerStyles ? { Inner: innerStyles } : null,
131+
);
132+
}, [styles, outerStyles, innerStyles]);
129133

130134
return (
131135
<ContentElement
Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
import { ForwardedRef, forwardRef } from 'react';
2+
3+
import { tasty } from '../../../tasty';
4+
5+
import { CubeLayoutContentProps, LayoutContent } from './LayoutContent';
6+
7+
const FlexElement = tasty(LayoutContent, {
8+
qa: 'LayoutFlex',
9+
styles: {
10+
flexShrink: 0,
11+
flexGrow: 0,
12+
13+
Inner: {
14+
display: 'flex',
15+
},
16+
},
17+
});
18+
19+
export interface CubeLayoutFlexProps extends CubeLayoutContentProps {}
20+
21+
function LayoutFlex(
22+
props: CubeLayoutFlexProps,
23+
ref: ForwardedRef<HTMLDivElement>,
24+
) {
25+
const { children, scrollbar = 'tiny', ...otherProps } = props;
26+
27+
return (
28+
<FlexElement {...otherProps} ref={ref} scrollbar={scrollbar}>
29+
{children}
30+
</FlexElement>
31+
);
32+
}
33+
34+
const _LayoutFlex = forwardRef(LayoutFlex);
35+
36+
_LayoutFlex.displayName = 'Layout.Flex';
37+
38+
export { _LayoutFlex as LayoutFlex };
Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,59 @@
1+
import { ForwardedRef, forwardRef } from 'react';
2+
3+
import { Styles, tasty } from '../../../tasty';
4+
5+
import { CubeLayoutContentProps, LayoutContent } from './LayoutContent';
6+
7+
const GridElement = tasty(LayoutContent, {
8+
qa: 'LayoutGrid',
9+
styles: {
10+
flexShrink: 0,
11+
flexGrow: 0,
12+
13+
Inner: {
14+
display: 'grid',
15+
},
16+
},
17+
});
18+
19+
export interface CubeLayoutGridProps extends CubeLayoutContentProps {
20+
/** Grid template columns */
21+
columns?: Styles['gridColumns'];
22+
/** Grid template rows */
23+
rows?: Styles['gridRows'];
24+
/** Grid template shorthand */
25+
template?: Styles['gridTemplate'];
26+
}
27+
28+
function LayoutGrid(
29+
props: CubeLayoutGridProps,
30+
ref: ForwardedRef<HTMLDivElement>,
31+
) {
32+
const {
33+
children,
34+
scrollbar = 'tiny',
35+
columns,
36+
rows,
37+
template,
38+
...otherProps
39+
} = props;
40+
41+
return (
42+
<GridElement
43+
{...otherProps}
44+
ref={ref}
45+
scrollbar={scrollbar}
46+
gridColumns={columns}
47+
gridRows={rows}
48+
gridTemplate={template}
49+
>
50+
{children}
51+
</GridElement>
52+
);
53+
}
54+
55+
const _LayoutGrid = forwardRef(LayoutGrid);
56+
57+
_LayoutGrid.displayName = 'Layout.Grid';
58+
59+
export { _LayoutGrid as LayoutGrid };

src/components/content/Layout/index.tsx

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,9 @@ import { ForwardRefExoticComponent, RefAttributes } from 'react';
33
import { CubeLayoutProps, Layout as LayoutBase } from './Layout';
44
import { LayoutBlock } from './LayoutBlock';
55
import { LayoutContent } from './LayoutContent';
6+
import { LayoutFlex } from './LayoutFlex';
67
import { LayoutFooter } from './LayoutFooter';
8+
import { LayoutGrid } from './LayoutGrid';
79
import { LayoutHeader } from './LayoutHeader';
810
import { LayoutPanel } from './LayoutPanel';
911
import { LayoutPanelHeader } from './LayoutPanelHeader';
@@ -14,6 +16,8 @@ export type { CubeGridLayoutProps } from './GridLayout';
1416
export type { CubeLayoutProps } from './Layout';
1517
export type { CubeLayoutBlockProps } from './LayoutBlock';
1618
export type { CubeLayoutContentProps, ScrollbarType } from './LayoutContent';
19+
export type { CubeLayoutFlexProps } from './LayoutFlex';
20+
export type { CubeLayoutGridProps } from './LayoutGrid';
1721
export type { CubeLayoutFooterProps } from './LayoutFooter';
1822
export type { CubeLayoutHeaderProps } from './LayoutHeader';
1923
export type { CubeLayoutPanelProps } from './LayoutPanel';
@@ -41,6 +45,8 @@ interface LayoutComponent
4145
Footer: typeof LayoutFooter;
4246
Content: typeof LayoutContent;
4347
Block: typeof LayoutBlock;
48+
Flex: typeof LayoutFlex;
49+
Grid: typeof LayoutGrid;
4450
Panel: typeof LayoutPanel;
4551
PanelHeader: typeof LayoutPanelHeader;
4652
}
@@ -51,6 +57,8 @@ const Layout = Object.assign(LayoutBase, {
5157
Footer: LayoutFooter,
5258
Content: LayoutContent,
5359
Block: LayoutBlock,
60+
Flex: LayoutFlex,
61+
Grid: LayoutGrid,
5462
Panel: LayoutPanel,
5563
PanelHeader: LayoutPanelHeader,
5664
}) as LayoutComponent;

src/tasty/styles/list.ts

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -83,3 +83,10 @@ export const CONTAINER_STYLES = [
8383
] as const;
8484

8585
export const OUTER_STYLES = [...POSITION_STYLES, ...DIMENSION_STYLES] as const;
86+
87+
export const INNER_STYLES = [
88+
...BASE_STYLES,
89+
...COLOR_STYLES,
90+
...BLOCK_STYLES,
91+
...FLOW_STYLES,
92+
] as const;

src/tasty/types.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ import {
88
CONTAINER_STYLES,
99
DIMENSION_STYLES,
1010
FLOW_STYLES,
11+
INNER_STYLES,
1112
OUTER_STYLES,
1213
POSITION_STYLES,
1314
TEXT_STYLES,
@@ -122,6 +123,7 @@ export type ContainerStyleProps = Pick<
122123
(typeof CONTAINER_STYLES)[number]
123124
>;
124125
export type OuterStyleProps = Pick<Styles, (typeof OUTER_STYLES)[number]>;
126+
export type InnerStyleProps = Pick<Styles, (typeof INNER_STYLES)[number]>;
125127

126128
export type ShortGridStyles = {
127129
template?: Styles['gridTemplate'];

0 commit comments

Comments
 (0)