Skip to content

Commit fc2f0ce

Browse files
nullcoderClaude
andcommitted
feat: implement Container component for consistent spacing (#62)
- Create Container component with 4 variants (default, narrow, wide, full) - Implement responsive padding scale (16px mobile, 32px tablet, 48px desktop) - Add prose typography support for text-heavy content - Support custom element types via 'as' prop - Create comprehensive tests with 100% coverage - Add interactive demo page showcasing all features The Container component provides consistent layout constraints and responsive spacing across the application, following the design specifications. Co-Authored-By: Claude <claude@ghostpaste.dev>
1 parent 795b949 commit fc2f0ce

File tree

3 files changed

+428
-0
lines changed

3 files changed

+428
-0
lines changed

app/demo/container/page.tsx

Lines changed: 230 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,230 @@
1+
"use client";
2+
3+
import { Container } from "@/components/ui/container";
4+
import {
5+
Card,
6+
CardContent,
7+
CardDescription,
8+
CardHeader,
9+
CardTitle,
10+
} from "@/components/ui/card";
11+
12+
export default function ContainerDemo() {
13+
return (
14+
<div className="space-y-12 py-8">
15+
{/* Hero Section */}
16+
<section className="bg-muted/50 py-12">
17+
<Container>
18+
<h1 className="text-4xl font-bold tracking-tight">
19+
Container Component Demo
20+
</h1>
21+
<p className="text-muted-foreground mt-4 text-lg">
22+
Explore the different container variants and their responsive
23+
behavior.
24+
</p>
25+
</Container>
26+
</section>
27+
28+
{/* Default Container */}
29+
<section>
30+
<Container>
31+
<Card>
32+
<CardHeader>
33+
<CardTitle>Default Container (max-width: 1280px)</CardTitle>
34+
<CardDescription>
35+
The standard container width for most content. Provides optimal
36+
reading experience on large screens while maintaining
37+
consistency.
38+
</CardDescription>
39+
</CardHeader>
40+
<CardContent>
41+
<div className="bg-muted h-32 rounded-md p-4">
42+
<p className="text-muted-foreground text-sm">
43+
Resize your browser to see responsive padding changes:
44+
<br />• Mobile: 16px padding
45+
<br />• Tablet (768px+): 32px padding
46+
<br />• Desktop (1024px+): 48px padding
47+
</p>
48+
</div>
49+
</CardContent>
50+
</Card>
51+
</Container>
52+
</section>
53+
54+
{/* Narrow Container */}
55+
<section>
56+
<Container variant="narrow">
57+
<Card>
58+
<CardHeader>
59+
<CardTitle>Narrow Container (max-width: 768px)</CardTitle>
60+
<CardDescription>
61+
Perfect for text-heavy content like documentation or blog posts.
62+
</CardDescription>
63+
</CardHeader>
64+
<CardContent>
65+
<div className="bg-muted h-32 rounded-md p-4">
66+
<p className="text-muted-foreground text-sm">
67+
This container is narrower, making it ideal for long-form text
68+
content where line length should be limited for better
69+
readability.
70+
</p>
71+
</div>
72+
</CardContent>
73+
</Card>
74+
</Container>
75+
</section>
76+
77+
{/* Wide Container */}
78+
<section>
79+
<Container variant="wide">
80+
<Card>
81+
<CardHeader>
82+
<CardTitle>Wide Container (max-width: 1536px)</CardTitle>
83+
<CardDescription>
84+
For content that needs more horizontal space, like dashboards or
85+
data tables.
86+
</CardDescription>
87+
</CardHeader>
88+
<CardContent>
89+
<div className="bg-muted h-32 rounded-md p-4">
90+
<p className="text-muted-foreground text-sm">
91+
This wider container provides more space for complex layouts
92+
and side-by-side content arrangements.
93+
</p>
94+
</div>
95+
</CardContent>
96+
</Card>
97+
</Container>
98+
</section>
99+
100+
{/* Full Width Container */}
101+
<section className="bg-muted/30 py-8">
102+
<Container variant="full">
103+
<Card>
104+
<CardHeader>
105+
<CardTitle>Full Width Container (max-width: 100%)</CardTitle>
106+
<CardDescription>
107+
Takes up the entire viewport width with consistent padding.
108+
</CardDescription>
109+
</CardHeader>
110+
<CardContent>
111+
<div className="bg-muted h-32 rounded-md p-4">
112+
<p className="text-muted-foreground text-sm">
113+
This container spans the full width of the viewport while
114+
maintaining the responsive padding scale. Useful for hero
115+
sections or full-bleed designs.
116+
</p>
117+
</div>
118+
</CardContent>
119+
</Card>
120+
</Container>
121+
</section>
122+
123+
{/* Prose Container */}
124+
<section>
125+
<Container variant="narrow" prose>
126+
<h2>Container with Prose Typography</h2>
127+
<p>
128+
When the <code>prose</code> prop is enabled, the container applies
129+
optimized typography styles for text content. This includes proper
130+
line heights, spacing between elements, and font sizes that enhance
131+
readability.
132+
</p>
133+
<h3>Why Use Prose?</h3>
134+
<p>
135+
The prose variant is perfect for documentation, blog posts, or any
136+
content where typography and readability are paramount. It
137+
automatically styles:
138+
</p>
139+
<ul>
140+
<li>Headings with appropriate sizes and spacing</li>
141+
<li>Paragraphs with optimal line height</li>
142+
<li>Lists with proper indentation</li>
143+
<li>Code blocks and inline code</li>
144+
<li>Links with consistent styling</li>
145+
</ul>
146+
<blockquote>
147+
<p>
148+
&ldquo;Good typography is invisible. Bad typography is
149+
everywhere.&rdquo;
150+
<br />
151+
<em>— Oliver Reichenstein</em>
152+
</p>
153+
</blockquote>
154+
<h3>Implementation</h3>
155+
<p>
156+
Simply add the <code>prose</code> prop to any container variant:
157+
</p>
158+
<pre>
159+
<code>{`<Container variant="narrow" prose>
160+
<h1>Your Article Title</h1>
161+
<p>Your content here...</p>
162+
</Container>`}</code>
163+
</pre>
164+
</Container>
165+
</section>
166+
167+
{/* Custom Styling Example */}
168+
<section>
169+
<Container className="py-12">
170+
<Card>
171+
<CardHeader>
172+
<CardTitle>Custom Styling</CardTitle>
173+
<CardDescription>
174+
The Container component accepts custom classes via the className
175+
prop.
176+
</CardDescription>
177+
</CardHeader>
178+
<CardContent>
179+
<p className="text-muted-foreground">
180+
This container has additional vertical padding applied through
181+
custom classes, demonstrating how you can extend the base styles
182+
while maintaining the responsive behavior.
183+
</p>
184+
</CardContent>
185+
</Card>
186+
</Container>
187+
</section>
188+
189+
{/* Nested Containers */}
190+
<section className="bg-muted/50 py-12">
191+
<Container>
192+
<h2 className="mb-6 text-2xl font-bold">Nested Containers</h2>
193+
<Container variant="narrow" className="bg-background rounded-lg p-8">
194+
<p className="text-muted-foreground">
195+
Containers can be nested when you need different max-widths within
196+
a section. This inner container has a narrow variant while the
197+
outer uses the default width.
198+
</p>
199+
</Container>
200+
</Container>
201+
</section>
202+
203+
{/* Semantic HTML Example */}
204+
<Container as="article" variant="narrow" prose className="my-12">
205+
<h2>Semantic HTML Support</h2>
206+
<p>
207+
The Container component supports the <code>as</code> prop, allowing
208+
you to render it as any HTML element. This example renders as an{" "}
209+
<code>&lt;article&gt;</code>
210+
element, improving semantic structure and accessibility.
211+
</p>
212+
<p>Common use cases include:</p>
213+
<ul>
214+
<li>
215+
<code>section</code> for page sections
216+
</li>
217+
<li>
218+
<code>article</code> for blog posts or articles
219+
</li>
220+
<li>
221+
<code>main</code> for main content areas
222+
</li>
223+
<li>
224+
<code>aside</code> for sidebars or related content
225+
</li>
226+
</ul>
227+
</Container>
228+
</div>
229+
);
230+
}

components/ui/container.test.tsx

Lines changed: 128 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,128 @@
1+
import { render } from "@testing-library/react";
2+
import { describe, it, expect } from "vitest";
3+
import { Container } from "./container";
4+
5+
describe("Container", () => {
6+
it("renders with default variant", () => {
7+
const { container } = render(
8+
<Container>
9+
<div>Test content</div>
10+
</Container>
11+
);
12+
13+
const element = container.firstChild as HTMLElement;
14+
expect(element).toHaveClass("max-w-screen-xl");
15+
expect(element).toHaveClass("mx-auto");
16+
expect(element).toHaveClass("px-4");
17+
expect(element).toHaveClass("md:px-8");
18+
expect(element).toHaveClass("lg:px-12");
19+
});
20+
21+
it("renders with narrow variant", () => {
22+
const { container } = render(
23+
<Container variant="narrow">
24+
<div>Test content</div>
25+
</Container>
26+
);
27+
28+
const element = container.firstChild as HTMLElement;
29+
expect(element).toHaveClass("max-w-3xl");
30+
});
31+
32+
it("renders with wide variant", () => {
33+
const { container } = render(
34+
<Container variant="wide">
35+
<div>Test content</div>
36+
</Container>
37+
);
38+
39+
const element = container.firstChild as HTMLElement;
40+
expect(element).toHaveClass("max-w-screen-2xl");
41+
});
42+
43+
it("renders with full variant", () => {
44+
const { container } = render(
45+
<Container variant="full">
46+
<div>Test content</div>
47+
</Container>
48+
);
49+
50+
const element = container.firstChild as HTMLElement;
51+
expect(element).toHaveClass("max-w-full");
52+
});
53+
54+
it("applies prose classes when prose prop is true", () => {
55+
const { container } = render(
56+
<Container prose>
57+
<h1>Title</h1>
58+
<p>Content</p>
59+
</Container>
60+
);
61+
62+
const element = container.firstChild as HTMLElement;
63+
expect(element).toHaveClass("prose");
64+
expect(element).toHaveClass("prose-slate");
65+
expect(element).toHaveClass("dark:prose-invert");
66+
});
67+
68+
it("accepts custom className", () => {
69+
const { container } = render(
70+
<Container className="custom-class">
71+
<div>Test content</div>
72+
</Container>
73+
);
74+
75+
const element = container.firstChild as HTMLElement;
76+
expect(element).toHaveClass("custom-class");
77+
});
78+
79+
it("renders with custom element type", () => {
80+
const { container } = render(
81+
<Container as="section">
82+
<div>Test content</div>
83+
</Container>
84+
);
85+
86+
const element = container.firstChild;
87+
expect(element?.nodeName).toBe("SECTION");
88+
});
89+
90+
it("passes through additional props", () => {
91+
const { container } = render(
92+
<Container data-testid="test-container" id="my-container">
93+
<div>Test content</div>
94+
</Container>
95+
);
96+
97+
const element = container.firstChild as HTMLElement;
98+
expect(element).toHaveAttribute("data-testid", "test-container");
99+
expect(element).toHaveAttribute("id", "my-container");
100+
});
101+
102+
it("renders children correctly", () => {
103+
const { getByText } = render(
104+
<Container>
105+
<h1>Test Title</h1>
106+
<p>Test paragraph</p>
107+
</Container>
108+
);
109+
110+
expect(getByText("Test Title")).toBeInTheDocument();
111+
expect(getByText("Test paragraph")).toBeInTheDocument();
112+
});
113+
114+
it("combines variant and prose classes correctly", () => {
115+
const { container } = render(
116+
<Container variant="narrow" prose className="additional-class">
117+
<p>Test content</p>
118+
</Container>
119+
);
120+
121+
const element = container.firstChild as HTMLElement;
122+
expect(element).toHaveClass("max-w-3xl");
123+
expect(element).toHaveClass("prose");
124+
expect(element).toHaveClass("additional-class");
125+
expect(element).toHaveClass("mx-auto");
126+
expect(element).toHaveClass("px-4");
127+
});
128+
});

0 commit comments

Comments
 (0)