Skip to content

Commit d79ba32

Browse files
committed
sidebar slots
1 parent 78fa1ed commit d79ba32

8 files changed

Lines changed: 265 additions & 153 deletions

File tree

src/layouts/Sidebar/Sidebar.stories.tsx

Lines changed: 105 additions & 60 deletions
Original file line numberDiff line numberDiff line change
@@ -11,9 +11,23 @@ const meta = {
1111
component:
1212
'Two column layout with main content and sidebar. Supports optional header, network selector, and configurable sidebar position.',
1313
},
14+
story: {
15+
inline: false,
16+
iframeHeight: 600,
17+
},
1418
},
1519
},
20+
decorators: [
21+
Story => (
22+
<div className="relative h-[600px] w-full">
23+
<Story />
24+
</div>
25+
),
26+
],
1627
argTypes: {
28+
children: {
29+
table: { disable: true },
30+
},
1731
showHeader: {
1832
control: 'boolean',
1933
description: 'Show the header with navigation',
@@ -32,23 +46,27 @@ const meta = {
3246
description: 'Position of the sidebar',
3347
},
3448
},
49+
tags: ['autodocs'],
3550
} satisfies Meta<typeof Sidebar>;
3651

3752
export default meta;
3853
type Story = StoryObj<typeof meta>;
3954

4055
/**
41-
* Default two-column layout with left sidebar
56+
* Default two-column layout with left sidebar using compound components pattern
4257
*/
4358
export const Default: Story = {
4459
args: {
60+
children: <></>,
4561
showHeader: false,
4662
showNetworkSelector: false,
4763
fullWidth: false,
4864
sidebarPosition: 'left',
49-
children: (
50-
<>
51-
<aside className="rounded-lg border border-slate-700 bg-slate-800 p-6">
65+
},
66+
render: args => (
67+
<Sidebar {...args}>
68+
<Sidebar.Aside>
69+
<div className="rounded-lg border border-slate-700 bg-slate-800 p-6">
5270
<h2 className="mb-4 text-xl font-semibold text-white">Sidebar</h2>
5371
<nav className="space-y-2">
5472
<a href="#" className="block rounded-md px-3 py-2 text-slate-300 hover:bg-slate-700">
@@ -61,32 +79,39 @@ export const Default: Story = {
6179
Navigation Item 3
6280
</a>
6381
</nav>
64-
</aside>
65-
<main className="rounded-lg border border-slate-700 bg-slate-800 p-8">
82+
</div>
83+
</Sidebar.Aside>
84+
<Sidebar.Main>
85+
<div className="rounded-lg border border-slate-700 bg-slate-800 p-8">
6686
<h1 className="mb-4 text-3xl font-bold text-white">Main Content</h1>
6787
<p className="text-slate-300">This is the main content area with sidebar on the left.</p>
68-
</main>
69-
</>
70-
),
71-
},
88+
</div>
89+
</Sidebar.Main>
90+
</Sidebar>
91+
),
7292
};
7393

7494
/**
75-
* Two-column layout with right sidebar
95+
* Two-column layout with right sidebar using compound components pattern
7696
*/
7797
export const RightSidebar: Story = {
7898
args: {
99+
children: <></>,
79100
showHeader: false,
80101
showNetworkSelector: false,
81102
fullWidth: false,
82103
sidebarPosition: 'right',
83-
children: (
84-
<>
85-
<main className="rounded-lg border border-slate-700 bg-slate-800 p-8">
104+
},
105+
render: args => (
106+
<Sidebar {...args}>
107+
<Sidebar.Main>
108+
<div className="rounded-lg border border-slate-700 bg-slate-800 p-8">
86109
<h1 className="mb-4 text-3xl font-bold text-white">Main Content</h1>
87110
<p className="text-slate-300">This is the main content area with sidebar on the right.</p>
88-
</main>
89-
<aside className="rounded-lg border border-slate-700 bg-slate-800 p-6">
111+
</div>
112+
</Sidebar.Main>
113+
<Sidebar.Aside>
114+
<div className="rounded-lg border border-slate-700 bg-slate-800 p-6">
90115
<h2 className="mb-4 text-xl font-semibold text-white">Sidebar</h2>
91116
<div className="space-y-4">
92117
<div className="rounded-md bg-slate-700 p-4">
@@ -96,24 +121,27 @@ export const RightSidebar: Story = {
96121
<p className="text-sm text-slate-300">Widget 2</p>
97122
</div>
98123
</div>
99-
</aside>
100-
</>
101-
),
102-
},
124+
</div>
125+
</Sidebar.Aside>
126+
</Sidebar>
127+
),
103128
};
104129

105130
/**
106-
* Layout with header and left sidebar
131+
* Layout with header and left sidebar using compound components pattern
107132
*/
108133
export const WithHeader: Story = {
109134
args: {
135+
children: <></>,
110136
showHeader: true,
111137
showNetworkSelector: false,
112138
fullWidth: false,
113139
sidebarPosition: 'left',
114-
children: (
115-
<>
116-
<aside className="rounded-lg border border-slate-700 bg-slate-800 p-6">
140+
},
141+
render: args => (
142+
<Sidebar {...args}>
143+
<Sidebar.Aside>
144+
<div className="rounded-lg border border-slate-700 bg-slate-800 p-6">
117145
<h2 className="mb-4 text-xl font-semibold text-white">Sidebar</h2>
118146
<nav className="space-y-2">
119147
<a href="#" className="block rounded-md px-3 py-2 text-slate-300 hover:bg-slate-700">
@@ -123,28 +151,33 @@ export const WithHeader: Story = {
123151
Navigation Item 2
124152
</a>
125153
</nav>
126-
</aside>
127-
<main className="rounded-lg border border-slate-700 bg-slate-800 p-8">
154+
</div>
155+
</Sidebar.Aside>
156+
<Sidebar.Main>
157+
<div className="rounded-lg border border-slate-700 bg-slate-800 p-8">
128158
<h1 className="mb-4 text-3xl font-bold text-white">Page with Header</h1>
129159
<p className="text-slate-300">This layout includes the header with navigation.</p>
130-
</main>
131-
</>
132-
),
133-
},
160+
</div>
161+
</Sidebar.Main>
162+
</Sidebar>
163+
),
134164
};
135165

136166
/**
137-
* Layout with header and network selector
167+
* Layout with header and network selector using compound components pattern
138168
*/
139169
export const WithHeaderAndNetworkSelector: Story = {
140170
args: {
171+
children: <></>,
141172
showHeader: true,
142173
showNetworkSelector: true,
143174
fullWidth: false,
144175
sidebarPosition: 'left',
145-
children: (
146-
<>
147-
<aside className="rounded-lg border border-slate-700 bg-slate-800 p-6">
176+
},
177+
render: args => (
178+
<Sidebar {...args}>
179+
<Sidebar.Aside>
180+
<div className="rounded-lg border border-slate-700 bg-slate-800 p-6">
148181
<h2 className="mb-4 text-xl font-semibold text-white">Filters</h2>
149182
<div className="space-y-4">
150183
<div>
@@ -156,52 +189,62 @@ export const WithHeaderAndNetworkSelector: Story = {
156189
</select>
157190
</div>
158191
</div>
159-
</aside>
160-
<main className="rounded-lg border border-slate-700 bg-slate-800 p-8">
192+
</div>
193+
</Sidebar.Aside>
194+
<Sidebar.Main>
195+
<div className="rounded-lg border border-slate-700 bg-slate-800 p-8">
161196
<h1 className="mb-4 text-3xl font-bold text-white">Content with Network Selector</h1>
162197
<p className="text-slate-300">This layout includes both header and network selector.</p>
163-
</main>
164-
</>
165-
),
166-
},
198+
</div>
199+
</Sidebar.Main>
200+
</Sidebar>
201+
),
167202
};
168203

169204
/**
170-
* Full width two-column layout
205+
* Full width two-column layout using compound components pattern
171206
*/
172207
export const FullWidth: Story = {
173208
args: {
209+
children: <></>,
174210
showHeader: true,
175211
showNetworkSelector: false,
176212
fullWidth: true,
177213
sidebarPosition: 'left',
178-
children: (
179-
<>
180-
<aside className="rounded-lg border border-slate-700 bg-slate-800 p-6">
214+
},
215+
render: args => (
216+
<Sidebar {...args}>
217+
<Sidebar.Aside>
218+
<div className="rounded-lg border border-slate-700 bg-slate-800 p-6">
181219
<h2 className="mb-4 text-xl font-semibold text-white">Sidebar</h2>
182220
<p className="text-slate-300">Fixed width sidebar</p>
183-
</aside>
184-
<main className="rounded-lg border border-slate-700 bg-slate-800 p-8">
221+
</div>
222+
</Sidebar.Aside>
223+
<Sidebar.Main>
224+
<div className="rounded-lg border border-slate-700 bg-slate-800 p-8">
185225
<h1 className="mb-4 text-3xl font-bold text-white">Full Width Layout</h1>
186226
<p className="text-slate-300">This layout uses full viewport width.</p>
187-
</main>
188-
</>
189-
),
190-
},
227+
</div>
228+
</Sidebar.Main>
229+
</Sidebar>
230+
),
191231
};
192232

193233
/**
194-
* Complex content with data visualization
234+
* Complex content with data visualization using compound components pattern
195235
*/
196236
export const WithComplexContent: Story = {
197237
args: {
238+
children: <></>,
198239
showHeader: true,
199240
showNetworkSelector: true,
200241
fullWidth: false,
201242
sidebarPosition: 'right',
202-
children: (
203-
<>
204-
<main className="space-y-6">
243+
},
244+
render: args => (
245+
<Sidebar {...args}>
246+
<Sidebar.Main>
247+
<div className="space-y-6">
205248
<div className="rounded-lg border border-slate-700 bg-slate-800 p-8">
206249
<h1 className="mb-4 text-3xl font-bold text-white">Dashboard</h1>
207250
<div className="grid grid-cols-1 gap-4 md:grid-cols-2">
@@ -214,8 +257,10 @@ export const WithComplexContent: Story = {
214257
))}
215258
</div>
216259
</div>
217-
</main>
218-
<aside className="space-y-4">
260+
</div>
261+
</Sidebar.Main>
262+
<Sidebar.Aside>
263+
<div className="space-y-4">
219264
<div className="rounded-lg border border-slate-700 bg-slate-800 p-6">
220265
<h2 className="mb-4 text-xl font-semibold text-white">Activity Feed</h2>
221266
<div className="space-y-3">
@@ -227,8 +272,8 @@ export const WithComplexContent: Story = {
227272
))}
228273
</div>
229274
</div>
230-
</aside>
231-
</>
232-
),
233-
},
275+
</div>
276+
</Sidebar.Aside>
277+
</Sidebar>
278+
),
234279
};

src/layouts/Sidebar/Sidebar.tsx

Lines changed: 32 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,26 +1,54 @@
1-
import { type JSX } from 'react';
1+
import { Children, type JSX, type ReactElement } from 'react';
22
import { Header } from '@/components/Header';
3-
import { type SidebarProps } from './Sidebar.types';
3+
import { type SidebarProps, type SidebarSlotProps } from './Sidebar.types';
44

5-
export function Sidebar({
5+
// Slot components - automatically wrap with semantic HTML tags
6+
function SidebarMain({ children }: SidebarSlotProps): JSX.Element {
7+
return <main>{children}</main>;
8+
}
9+
10+
function SidebarAside({ children }: SidebarSlotProps): JSX.Element {
11+
return <aside>{children}</aside>;
12+
}
13+
14+
// Main Sidebar component
15+
function SidebarComponent({
616
children,
717
showHeader = false,
818
showNetworkSelector = false,
919
fullWidth = false,
1020
sidebarPosition = 'left',
1121
}: SidebarProps): JSX.Element {
22+
// Filter children to find Main and Aside slots
23+
const childArray = Children.toArray(children) as ReactElement[];
24+
const mainSlot = childArray.find(child => child.type === SidebarMain);
25+
const asideSlot = childArray.find(child => child.type === SidebarAside);
26+
1227
// Determine grid columns based on sidebar position
1328
const gridCols = sidebarPosition === 'left' ? 'grid-cols-[300px_1fr]' : 'grid-cols-[1fr_300px]';
1429

30+
// Render slots in correct order based on position
31+
const leftContent = sidebarPosition === 'left' ? asideSlot : mainSlot;
32+
const rightContent = sidebarPosition === 'left' ? mainSlot : asideSlot;
33+
1534
return (
1635
<div className="min-h-dvh bg-slate-900">
1736
{/* Header - conditional */}
1837
{showHeader && <Header showNetworkSelector={showNetworkSelector} />}
1938

2039
{/* Two Column Grid */}
2140
<div className={fullWidth ? 'px-4 py-8' : 'mx-auto max-w-7xl px-4 py-8 sm:px-6 lg:px-8'}>
22-
<div className={`grid ${gridCols} gap-6`}>{children}</div>
41+
<div className={`grid ${gridCols} gap-6`}>
42+
{leftContent}
43+
{rightContent}
44+
</div>
2345
</div>
2446
</div>
2547
);
2648
}
49+
50+
// Attach slot components to main component
51+
export const Sidebar = Object.assign(SidebarComponent, {
52+
Main: SidebarMain,
53+
Aside: SidebarAside,
54+
});

src/layouts/Sidebar/Sidebar.types.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,3 +7,7 @@ export interface SidebarProps {
77
fullWidth?: boolean;
88
sidebarPosition?: 'left' | 'right';
99
}
10+
11+
export interface SidebarSlotProps {
12+
children: ReactNode;
13+
}

src/layouts/Standard/Standard.stories.tsx

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,8 +10,19 @@ const meta = {
1010
description: {
1111
component: 'Single column layout with optional header and network selector. Supports full-width content.',
1212
},
13+
story: {
14+
inline: false,
15+
iframeHeight: 600,
16+
},
1317
},
1418
},
19+
decorators: [
20+
Story => (
21+
<div className="relative h-[600px] w-full">
22+
<Story />
23+
</div>
24+
),
25+
],
1526
argTypes: {
1627
showHeader: {
1728
control: 'boolean',
@@ -26,6 +37,7 @@ const meta = {
2637
description: 'Use full width instead of max-width container',
2738
},
2839
},
40+
tags: ['autodocs'],
2941
} satisfies Meta<typeof Standard>;
3042

3143
export default meta;

0 commit comments

Comments
 (0)