omgkit 2.1.0 → 2.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +1 -1
- package/plugin/skills/SKILL_STANDARDS.md +743 -0
- package/plugin/skills/databases/mongodb/SKILL.md +797 -28
- package/plugin/skills/databases/postgresql/SKILL.md +494 -18
- package/plugin/skills/databases/prisma/SKILL.md +776 -30
- package/plugin/skills/databases/redis/SKILL.md +885 -25
- package/plugin/skills/devops/aws/SKILL.md +686 -28
- package/plugin/skills/devops/docker/SKILL.md +466 -18
- package/plugin/skills/devops/github-actions/SKILL.md +684 -29
- package/plugin/skills/devops/kubernetes/SKILL.md +621 -24
- package/plugin/skills/frameworks/django/SKILL.md +920 -20
- package/plugin/skills/frameworks/express/SKILL.md +1361 -35
- package/plugin/skills/frameworks/fastapi/SKILL.md +1260 -33
- package/plugin/skills/frameworks/laravel/SKILL.md +1244 -31
- package/plugin/skills/frameworks/nestjs/SKILL.md +1005 -26
- package/plugin/skills/frameworks/nextjs/SKILL.md +407 -44
- package/plugin/skills/frameworks/rails/SKILL.md +594 -28
- package/plugin/skills/frameworks/react/SKILL.md +1006 -32
- package/plugin/skills/frameworks/spring/SKILL.md +528 -35
- package/plugin/skills/frameworks/vue/SKILL.md +1296 -27
- package/plugin/skills/frontend/accessibility/SKILL.md +1108 -34
- package/plugin/skills/frontend/frontend-design/SKILL.md +1304 -26
- package/plugin/skills/frontend/responsive/SKILL.md +847 -21
- package/plugin/skills/frontend/shadcn-ui/SKILL.md +976 -38
- package/plugin/skills/frontend/tailwindcss/SKILL.md +831 -35
- package/plugin/skills/frontend/threejs/SKILL.md +1298 -29
- package/plugin/skills/languages/javascript/SKILL.md +935 -31
- package/plugin/skills/languages/python/SKILL.md +489 -25
- package/plugin/skills/languages/typescript/SKILL.md +379 -30
- package/plugin/skills/methodology/brainstorming/SKILL.md +597 -23
- package/plugin/skills/methodology/defense-in-depth/SKILL.md +832 -34
- package/plugin/skills/methodology/dispatching-parallel-agents/SKILL.md +665 -31
- package/plugin/skills/methodology/executing-plans/SKILL.md +556 -24
- package/plugin/skills/methodology/finishing-development-branch/SKILL.md +595 -25
- package/plugin/skills/methodology/problem-solving/SKILL.md +429 -61
- package/plugin/skills/methodology/receiving-code-review/SKILL.md +536 -24
- package/plugin/skills/methodology/requesting-code-review/SKILL.md +632 -21
- package/plugin/skills/methodology/root-cause-tracing/SKILL.md +641 -30
- package/plugin/skills/methodology/sequential-thinking/SKILL.md +262 -3
- package/plugin/skills/methodology/systematic-debugging/SKILL.md +571 -32
- package/plugin/skills/methodology/test-driven-development/SKILL.md +779 -24
- package/plugin/skills/methodology/testing-anti-patterns/SKILL.md +691 -29
- package/plugin/skills/methodology/token-optimization/SKILL.md +598 -29
- package/plugin/skills/methodology/verification-before-completion/SKILL.md +543 -22
- package/plugin/skills/methodology/writing-plans/SKILL.md +590 -18
- package/plugin/skills/omega/omega-architecture/SKILL.md +838 -39
- package/plugin/skills/omega/omega-coding/SKILL.md +636 -39
- package/plugin/skills/omega/omega-sprint/SKILL.md +855 -48
- package/plugin/skills/omega/omega-testing/SKILL.md +940 -41
- package/plugin/skills/omega/omega-thinking/SKILL.md +703 -50
- package/plugin/skills/security/better-auth/SKILL.md +1065 -28
- package/plugin/skills/security/oauth/SKILL.md +968 -31
- package/plugin/skills/security/owasp/SKILL.md +894 -33
- package/plugin/skills/testing/playwright/SKILL.md +764 -38
- package/plugin/skills/testing/pytest/SKILL.md +873 -36
- package/plugin/skills/testing/vitest/SKILL.md +980 -35
|
@@ -1,46 +1,872 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: responsive
|
|
3
|
-
description: Responsive design
|
|
3
|
+
description: Responsive web design with mobile-first approach, fluid layouts, container queries, and cross-device testing
|
|
4
|
+
category: frontend
|
|
5
|
+
triggers:
|
|
6
|
+
- responsive
|
|
7
|
+
- responsive design
|
|
8
|
+
- mobile first
|
|
9
|
+
- adaptive layout
|
|
10
|
+
- breakpoints
|
|
11
|
+
- media queries
|
|
4
12
|
---
|
|
5
13
|
|
|
6
|
-
# Responsive Design
|
|
14
|
+
# Responsive Design
|
|
15
|
+
|
|
16
|
+
Enterprise-grade **responsive web design** following industry best practices. This skill covers mobile-first development, fluid layouts, container queries, responsive images, and cross-device optimization patterns used by top engineering teams.
|
|
17
|
+
|
|
18
|
+
## Purpose
|
|
19
|
+
|
|
20
|
+
Build seamless cross-device experiences:
|
|
21
|
+
|
|
22
|
+
- Implement mobile-first design approach
|
|
23
|
+
- Create fluid, flexible layouts
|
|
24
|
+
- Use modern CSS layout techniques
|
|
25
|
+
- Optimize images for all devices
|
|
26
|
+
- Implement container queries
|
|
27
|
+
- Test across devices and viewports
|
|
28
|
+
- Handle touch and mouse interactions
|
|
29
|
+
|
|
30
|
+
## Features
|
|
31
|
+
|
|
32
|
+
### 1. Mobile-First Breakpoints
|
|
7
33
|
|
|
8
|
-
## Breakpoints
|
|
9
34
|
```css
|
|
10
|
-
/*
|
|
11
|
-
|
|
35
|
+
/* Base styles (mobile-first) */
|
|
36
|
+
:root {
|
|
37
|
+
/* Breakpoint values */
|
|
38
|
+
--breakpoint-sm: 640px;
|
|
39
|
+
--breakpoint-md: 768px;
|
|
40
|
+
--breakpoint-lg: 1024px;
|
|
41
|
+
--breakpoint-xl: 1280px;
|
|
42
|
+
--breakpoint-2xl: 1536px;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/* Mobile base styles */
|
|
46
|
+
.container {
|
|
47
|
+
width: 100%;
|
|
48
|
+
padding-left: 1rem;
|
|
49
|
+
padding-right: 1rem;
|
|
50
|
+
margin-left: auto;
|
|
51
|
+
margin-right: auto;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/* Small devices (640px and up) */
|
|
55
|
+
@media (min-width: 640px) {
|
|
56
|
+
.container {
|
|
57
|
+
max-width: 640px;
|
|
58
|
+
padding-left: 1.5rem;
|
|
59
|
+
padding-right: 1.5rem;
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/* Medium devices (768px and up) */
|
|
64
|
+
@media (min-width: 768px) {
|
|
65
|
+
.container {
|
|
66
|
+
max-width: 768px;
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/* Large devices (1024px and up) */
|
|
71
|
+
@media (min-width: 1024px) {
|
|
72
|
+
.container {
|
|
73
|
+
max-width: 1024px;
|
|
74
|
+
padding-left: 2rem;
|
|
75
|
+
padding-right: 2rem;
|
|
76
|
+
}
|
|
77
|
+
}
|
|
12
78
|
|
|
13
|
-
|
|
14
|
-
@media (min-width:
|
|
15
|
-
|
|
16
|
-
|
|
79
|
+
/* Extra large devices (1280px and up) */
|
|
80
|
+
@media (min-width: 1280px) {
|
|
81
|
+
.container {
|
|
82
|
+
max-width: 1280px;
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
/* 2XL devices (1536px and up) */
|
|
87
|
+
@media (min-width: 1536px) {
|
|
88
|
+
.container {
|
|
89
|
+
max-width: 1536px;
|
|
90
|
+
}
|
|
91
|
+
}
|
|
17
92
|
```
|
|
18
93
|
|
|
19
|
-
|
|
94
|
+
```typescript
|
|
95
|
+
// hooks/useBreakpoint.ts
|
|
96
|
+
import { useState, useEffect } from "react";
|
|
97
|
+
|
|
98
|
+
const breakpoints = {
|
|
99
|
+
sm: 640,
|
|
100
|
+
md: 768,
|
|
101
|
+
lg: 1024,
|
|
102
|
+
xl: 1280,
|
|
103
|
+
"2xl": 1536,
|
|
104
|
+
} as const;
|
|
105
|
+
|
|
106
|
+
type Breakpoint = keyof typeof breakpoints;
|
|
107
|
+
|
|
108
|
+
export function useBreakpoint() {
|
|
109
|
+
const [breakpoint, setBreakpoint] = useState<Breakpoint | null>(null);
|
|
110
|
+
const [width, setWidth] = useState(0);
|
|
111
|
+
|
|
112
|
+
useEffect(() => {
|
|
113
|
+
const handleResize = () => {
|
|
114
|
+
const w = window.innerWidth;
|
|
115
|
+
setWidth(w);
|
|
116
|
+
|
|
117
|
+
if (w >= breakpoints["2xl"]) setBreakpoint("2xl");
|
|
118
|
+
else if (w >= breakpoints.xl) setBreakpoint("xl");
|
|
119
|
+
else if (w >= breakpoints.lg) setBreakpoint("lg");
|
|
120
|
+
else if (w >= breakpoints.md) setBreakpoint("md");
|
|
121
|
+
else if (w >= breakpoints.sm) setBreakpoint("sm");
|
|
122
|
+
else setBreakpoint(null);
|
|
123
|
+
};
|
|
124
|
+
|
|
125
|
+
handleResize();
|
|
126
|
+
window.addEventListener("resize", handleResize);
|
|
127
|
+
return () => window.removeEventListener("resize", handleResize);
|
|
128
|
+
}, []);
|
|
129
|
+
|
|
130
|
+
const isAbove = (bp: Breakpoint) => width >= breakpoints[bp];
|
|
131
|
+
const isBelow = (bp: Breakpoint) => width < breakpoints[bp];
|
|
132
|
+
|
|
133
|
+
return { breakpoint, width, isAbove, isBelow };
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
// hooks/useMediaQuery.ts
|
|
137
|
+
import { useState, useEffect } from "react";
|
|
138
|
+
|
|
139
|
+
export function useMediaQuery(query: string): boolean {
|
|
140
|
+
const [matches, setMatches] = useState(false);
|
|
141
|
+
|
|
142
|
+
useEffect(() => {
|
|
143
|
+
const media = window.matchMedia(query);
|
|
144
|
+
if (media.matches !== matches) {
|
|
145
|
+
setMatches(media.matches);
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
const listener = (e: MediaQueryListEvent) => setMatches(e.matches);
|
|
149
|
+
media.addEventListener("change", listener);
|
|
150
|
+
return () => media.removeEventListener("change", listener);
|
|
151
|
+
}, [matches, query]);
|
|
152
|
+
|
|
153
|
+
return matches;
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
// Usage
|
|
157
|
+
function Component() {
|
|
158
|
+
const isMobile = useMediaQuery("(max-width: 639px)");
|
|
159
|
+
const isTablet = useMediaQuery("(min-width: 640px) and (max-width: 1023px)");
|
|
160
|
+
const isDesktop = useMediaQuery("(min-width: 1024px)");
|
|
161
|
+
const prefersDark = useMediaQuery("(prefers-color-scheme: dark)");
|
|
162
|
+
const prefersReducedMotion = useMediaQuery("(prefers-reduced-motion: reduce)");
|
|
163
|
+
|
|
164
|
+
return (
|
|
165
|
+
<div>
|
|
166
|
+
{isMobile && <MobileLayout />}
|
|
167
|
+
{isTablet && <TabletLayout />}
|
|
168
|
+
{isDesktop && <DesktopLayout />}
|
|
169
|
+
</div>
|
|
170
|
+
);
|
|
171
|
+
}
|
|
172
|
+
```
|
|
173
|
+
|
|
174
|
+
### 2. Fluid Typography
|
|
20
175
|
|
|
21
|
-
### Fluid Typography
|
|
22
176
|
```css
|
|
23
|
-
|
|
177
|
+
/* Fluid typography using clamp() */
|
|
178
|
+
:root {
|
|
179
|
+
/* Base font sizes */
|
|
180
|
+
--font-size-xs: clamp(0.75rem, 0.7rem + 0.25vw, 0.875rem);
|
|
181
|
+
--font-size-sm: clamp(0.875rem, 0.8rem + 0.35vw, 1rem);
|
|
182
|
+
--font-size-base: clamp(1rem, 0.9rem + 0.5vw, 1.125rem);
|
|
183
|
+
--font-size-lg: clamp(1.125rem, 1rem + 0.6vw, 1.25rem);
|
|
184
|
+
--font-size-xl: clamp(1.25rem, 1.1rem + 0.75vw, 1.5rem);
|
|
185
|
+
--font-size-2xl: clamp(1.5rem, 1.25rem + 1.25vw, 2rem);
|
|
186
|
+
--font-size-3xl: clamp(1.875rem, 1.5rem + 1.875vw, 2.5rem);
|
|
187
|
+
--font-size-4xl: clamp(2.25rem, 1.75rem + 2.5vw, 3rem);
|
|
188
|
+
--font-size-5xl: clamp(3rem, 2rem + 5vw, 4rem);
|
|
189
|
+
|
|
190
|
+
/* Fluid spacing */
|
|
191
|
+
--space-xs: clamp(0.25rem, 0.2rem + 0.25vw, 0.5rem);
|
|
192
|
+
--space-sm: clamp(0.5rem, 0.4rem + 0.5vw, 0.75rem);
|
|
193
|
+
--space-md: clamp(1rem, 0.8rem + 1vw, 1.5rem);
|
|
194
|
+
--space-lg: clamp(1.5rem, 1.2rem + 1.5vw, 2.5rem);
|
|
195
|
+
--space-xl: clamp(2rem, 1.5rem + 2.5vw, 4rem);
|
|
196
|
+
--space-2xl: clamp(3rem, 2rem + 5vw, 6rem);
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
/* Typography scale */
|
|
200
|
+
h1 {
|
|
201
|
+
font-size: var(--font-size-4xl);
|
|
202
|
+
line-height: 1.1;
|
|
203
|
+
letter-spacing: -0.02em;
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
h2 {
|
|
207
|
+
font-size: var(--font-size-3xl);
|
|
208
|
+
line-height: 1.2;
|
|
209
|
+
letter-spacing: -0.01em;
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
h3 {
|
|
213
|
+
font-size: var(--font-size-2xl);
|
|
214
|
+
line-height: 1.3;
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
h4 {
|
|
218
|
+
font-size: var(--font-size-xl);
|
|
219
|
+
line-height: 1.4;
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
p {
|
|
223
|
+
font-size: var(--font-size-base);
|
|
224
|
+
line-height: 1.6;
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
/* Responsive text utilities */
|
|
228
|
+
.text-balance {
|
|
229
|
+
text-wrap: balance;
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
.text-pretty {
|
|
233
|
+
text-wrap: pretty;
|
|
234
|
+
}
|
|
235
|
+
```
|
|
236
|
+
|
|
237
|
+
### 3. Flexible Grid Layouts
|
|
238
|
+
|
|
239
|
+
```css
|
|
240
|
+
/* Auto-fit responsive grid */
|
|
241
|
+
.auto-grid {
|
|
242
|
+
display: grid;
|
|
243
|
+
grid-template-columns: repeat(auto-fit, minmax(min(300px, 100%), 1fr));
|
|
244
|
+
gap: var(--space-md);
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
/* Auto-fill variant */
|
|
248
|
+
.auto-fill-grid {
|
|
249
|
+
display: grid;
|
|
250
|
+
grid-template-columns: repeat(auto-fill, minmax(250px, 1fr));
|
|
251
|
+
gap: var(--space-md);
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
/* Sidebar layout */
|
|
255
|
+
.sidebar-layout {
|
|
256
|
+
display: grid;
|
|
257
|
+
grid-template-columns: 1fr;
|
|
258
|
+
gap: var(--space-lg);
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
@media (min-width: 768px) {
|
|
262
|
+
.sidebar-layout {
|
|
263
|
+
grid-template-columns: minmax(200px, 25%) 1fr;
|
|
264
|
+
}
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
/* Holy grail layout */
|
|
268
|
+
.holy-grail {
|
|
269
|
+
display: grid;
|
|
270
|
+
grid-template-rows: auto 1fr auto;
|
|
271
|
+
min-height: 100vh;
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
.holy-grail-main {
|
|
275
|
+
display: grid;
|
|
276
|
+
grid-template-columns: 1fr;
|
|
277
|
+
gap: var(--space-md);
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
@media (min-width: 768px) {
|
|
281
|
+
.holy-grail-main {
|
|
282
|
+
grid-template-columns: minmax(150px, 20%) 1fr;
|
|
283
|
+
}
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
@media (min-width: 1024px) {
|
|
287
|
+
.holy-grail-main {
|
|
288
|
+
grid-template-columns: minmax(200px, 20%) 1fr minmax(200px, 20%);
|
|
289
|
+
}
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
/* Masonry-like layout with CSS Grid */
|
|
293
|
+
.masonry-grid {
|
|
294
|
+
display: grid;
|
|
295
|
+
grid-template-columns: repeat(auto-fill, minmax(250px, 1fr));
|
|
296
|
+
grid-auto-rows: 10px;
|
|
297
|
+
gap: var(--space-md);
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
/* Card spans based on content */
|
|
301
|
+
.masonry-item--small {
|
|
302
|
+
grid-row: span 20;
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
.masonry-item--medium {
|
|
306
|
+
grid-row: span 30;
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
.masonry-item--large {
|
|
310
|
+
grid-row: span 40;
|
|
311
|
+
}
|
|
312
|
+
```
|
|
313
|
+
|
|
314
|
+
```tsx
|
|
315
|
+
// Responsive grid component
|
|
316
|
+
interface ResponsiveGridProps {
|
|
317
|
+
children: React.ReactNode;
|
|
318
|
+
minWidth?: string;
|
|
319
|
+
gap?: string;
|
|
320
|
+
className?: string;
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
export function ResponsiveGrid({
|
|
324
|
+
children,
|
|
325
|
+
minWidth = "300px",
|
|
326
|
+
gap = "1rem",
|
|
327
|
+
className = "",
|
|
328
|
+
}: ResponsiveGridProps) {
|
|
329
|
+
return (
|
|
330
|
+
<div
|
|
331
|
+
className={className}
|
|
332
|
+
style={{
|
|
333
|
+
display: "grid",
|
|
334
|
+
gridTemplateColumns: `repeat(auto-fit, minmax(min(${minWidth}, 100%), 1fr))`,
|
|
335
|
+
gap,
|
|
336
|
+
}}
|
|
337
|
+
>
|
|
338
|
+
{children}
|
|
339
|
+
</div>
|
|
340
|
+
);
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
// Usage
|
|
344
|
+
function ProductGrid() {
|
|
345
|
+
return (
|
|
346
|
+
<ResponsiveGrid minWidth="280px" gap="1.5rem">
|
|
347
|
+
{products.map((product) => (
|
|
348
|
+
<ProductCard key={product.id} product={product} />
|
|
349
|
+
))}
|
|
350
|
+
</ResponsiveGrid>
|
|
351
|
+
);
|
|
352
|
+
}
|
|
24
353
|
```
|
|
25
354
|
|
|
26
|
-
###
|
|
355
|
+
### 4. Container Queries
|
|
356
|
+
|
|
27
357
|
```css
|
|
28
|
-
|
|
358
|
+
/* Container query setup */
|
|
359
|
+
.card-container {
|
|
360
|
+
container-type: inline-size;
|
|
361
|
+
container-name: card;
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
/* Base card styles (smallest size) */
|
|
365
|
+
.card {
|
|
366
|
+
display: flex;
|
|
367
|
+
flex-direction: column;
|
|
368
|
+
padding: 1rem;
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
.card-image {
|
|
372
|
+
width: 100%;
|
|
373
|
+
aspect-ratio: 16/9;
|
|
374
|
+
object-fit: cover;
|
|
375
|
+
}
|
|
376
|
+
|
|
377
|
+
.card-content {
|
|
378
|
+
padding: 1rem 0;
|
|
379
|
+
}
|
|
380
|
+
|
|
381
|
+
/* Card layout changes based on container width */
|
|
382
|
+
@container card (min-width: 400px) {
|
|
383
|
+
.card {
|
|
384
|
+
flex-direction: row;
|
|
385
|
+
gap: 1rem;
|
|
386
|
+
}
|
|
387
|
+
|
|
388
|
+
.card-image {
|
|
389
|
+
width: 40%;
|
|
390
|
+
aspect-ratio: 1;
|
|
391
|
+
}
|
|
392
|
+
|
|
393
|
+
.card-content {
|
|
394
|
+
flex: 1;
|
|
395
|
+
display: flex;
|
|
396
|
+
flex-direction: column;
|
|
397
|
+
justify-content: center;
|
|
398
|
+
}
|
|
399
|
+
}
|
|
400
|
+
|
|
401
|
+
@container card (min-width: 600px) {
|
|
402
|
+
.card {
|
|
403
|
+
padding: 1.5rem;
|
|
404
|
+
}
|
|
405
|
+
|
|
406
|
+
.card-image {
|
|
407
|
+
width: 35%;
|
|
408
|
+
}
|
|
409
|
+
|
|
410
|
+
.card-title {
|
|
411
|
+
font-size: 1.5rem;
|
|
412
|
+
}
|
|
413
|
+
}
|
|
414
|
+
|
|
415
|
+
/* Container query units */
|
|
416
|
+
.responsive-text {
|
|
417
|
+
font-size: clamp(1rem, 3cqi, 1.5rem);
|
|
418
|
+
padding: 2cqi;
|
|
419
|
+
}
|
|
420
|
+
|
|
421
|
+
/* Style queries (experimental) */
|
|
422
|
+
@container style(--theme: dark) {
|
|
423
|
+
.card {
|
|
424
|
+
background: #1a1a1a;
|
|
425
|
+
color: white;
|
|
426
|
+
}
|
|
427
|
+
}
|
|
428
|
+
```
|
|
429
|
+
|
|
430
|
+
```tsx
|
|
431
|
+
// React component with container queries
|
|
432
|
+
export function ContainerQueryCard({ title, description, image }: CardProps) {
|
|
433
|
+
return (
|
|
434
|
+
<div className="card-container">
|
|
435
|
+
<article className="card">
|
|
436
|
+
<img src={image} alt="" className="card-image" />
|
|
437
|
+
<div className="card-content">
|
|
438
|
+
<h3 className="card-title">{title}</h3>
|
|
439
|
+
<p className="card-description">{description}</p>
|
|
440
|
+
</div>
|
|
441
|
+
</article>
|
|
442
|
+
</div>
|
|
443
|
+
);
|
|
444
|
+
}
|
|
445
|
+
|
|
446
|
+
// CSS Module with container queries
|
|
447
|
+
// Card.module.css
|
|
448
|
+
/*
|
|
449
|
+
.container {
|
|
450
|
+
container-type: inline-size;
|
|
451
|
+
}
|
|
452
|
+
|
|
453
|
+
.card {
|
|
29
454
|
display: grid;
|
|
30
|
-
grid-template-columns: repeat(auto-fit, minmax(300px, 1fr));
|
|
31
455
|
gap: 1rem;
|
|
32
456
|
}
|
|
457
|
+
|
|
458
|
+
@container (min-width: 30rem) {
|
|
459
|
+
.card {
|
|
460
|
+
grid-template-columns: 1fr 2fr;
|
|
461
|
+
}
|
|
462
|
+
}
|
|
463
|
+
*/
|
|
33
464
|
```
|
|
34
465
|
|
|
35
|
-
###
|
|
466
|
+
### 5. Responsive Images
|
|
467
|
+
|
|
468
|
+
```tsx
|
|
469
|
+
// Responsive image component
|
|
470
|
+
interface ResponsiveImageProps {
|
|
471
|
+
src: string;
|
|
472
|
+
alt: string;
|
|
473
|
+
sizes?: string;
|
|
474
|
+
className?: string;
|
|
475
|
+
priority?: boolean;
|
|
476
|
+
}
|
|
477
|
+
|
|
478
|
+
export function ResponsiveImage({
|
|
479
|
+
src,
|
|
480
|
+
alt,
|
|
481
|
+
sizes = "100vw",
|
|
482
|
+
className,
|
|
483
|
+
priority = false,
|
|
484
|
+
}: ResponsiveImageProps) {
|
|
485
|
+
// Generate srcset for different sizes
|
|
486
|
+
const widths = [320, 640, 768, 1024, 1280, 1536, 1920];
|
|
487
|
+
const srcSet = widths
|
|
488
|
+
.map((w) => `${src}?w=${w} ${w}w`)
|
|
489
|
+
.join(", ");
|
|
490
|
+
|
|
491
|
+
return (
|
|
492
|
+
<img
|
|
493
|
+
src={src}
|
|
494
|
+
srcSet={srcSet}
|
|
495
|
+
sizes={sizes}
|
|
496
|
+
alt={alt}
|
|
497
|
+
className={className}
|
|
498
|
+
loading={priority ? "eager" : "lazy"}
|
|
499
|
+
decoding={priority ? "sync" : "async"}
|
|
500
|
+
/>
|
|
501
|
+
);
|
|
502
|
+
}
|
|
503
|
+
|
|
504
|
+
// Picture element for art direction
|
|
505
|
+
export function ArtDirectedImage({
|
|
506
|
+
mobileSrc,
|
|
507
|
+
tabletSrc,
|
|
508
|
+
desktopSrc,
|
|
509
|
+
alt,
|
|
510
|
+
}: {
|
|
511
|
+
mobileSrc: string;
|
|
512
|
+
tabletSrc: string;
|
|
513
|
+
desktopSrc: string;
|
|
514
|
+
alt: string;
|
|
515
|
+
}) {
|
|
516
|
+
return (
|
|
517
|
+
<picture>
|
|
518
|
+
{/* Desktop - landscape image */}
|
|
519
|
+
<source media="(min-width: 1024px)" srcSet={desktopSrc} />
|
|
520
|
+
{/* Tablet - square image */}
|
|
521
|
+
<source media="(min-width: 640px)" srcSet={tabletSrc} />
|
|
522
|
+
{/* Mobile - portrait image (default) */}
|
|
523
|
+
<img src={mobileSrc} alt={alt} loading="lazy" />
|
|
524
|
+
</picture>
|
|
525
|
+
);
|
|
526
|
+
}
|
|
527
|
+
|
|
528
|
+
// Background image with responsive behavior
|
|
529
|
+
export function ResponsiveHero({ children }: { children: React.ReactNode }) {
|
|
530
|
+
return (
|
|
531
|
+
<section
|
|
532
|
+
className="
|
|
533
|
+
relative min-h-[50vh] md:min-h-[60vh] lg:min-h-[70vh]
|
|
534
|
+
bg-cover bg-center bg-no-repeat
|
|
535
|
+
bg-[image:var(--mobile-bg)]
|
|
536
|
+
md:bg-[image:var(--tablet-bg)]
|
|
537
|
+
lg:bg-[image:var(--desktop-bg)]
|
|
538
|
+
"
|
|
539
|
+
style={{
|
|
540
|
+
"--mobile-bg": "url('/hero-mobile.jpg')",
|
|
541
|
+
"--tablet-bg": "url('/hero-tablet.jpg')",
|
|
542
|
+
"--desktop-bg": "url('/hero-desktop.jpg')",
|
|
543
|
+
} as React.CSSProperties}
|
|
544
|
+
>
|
|
545
|
+
<div className="absolute inset-0 bg-black/50" />
|
|
546
|
+
<div className="relative z-10 flex items-center justify-center h-full">
|
|
547
|
+
{children}
|
|
548
|
+
</div>
|
|
549
|
+
</section>
|
|
550
|
+
);
|
|
551
|
+
}
|
|
552
|
+
```
|
|
553
|
+
|
|
554
|
+
### 6. Touch-Friendly Design
|
|
555
|
+
|
|
36
556
|
```css
|
|
37
|
-
|
|
38
|
-
|
|
557
|
+
/* Touch target sizes (minimum 44x44px) */
|
|
558
|
+
.touch-target {
|
|
559
|
+
min-width: 44px;
|
|
560
|
+
min-height: 44px;
|
|
561
|
+
display: flex;
|
|
562
|
+
align-items: center;
|
|
563
|
+
justify-content: center;
|
|
564
|
+
}
|
|
565
|
+
|
|
566
|
+
/* Larger touch targets on mobile */
|
|
567
|
+
@media (pointer: coarse) {
|
|
568
|
+
.button,
|
|
569
|
+
.link,
|
|
570
|
+
.interactive {
|
|
571
|
+
min-height: 48px;
|
|
572
|
+
padding: 12px 16px;
|
|
573
|
+
}
|
|
574
|
+
|
|
575
|
+
/* Increase spacing between interactive elements */
|
|
576
|
+
.button-group {
|
|
577
|
+
gap: 12px;
|
|
578
|
+
}
|
|
579
|
+
|
|
580
|
+
/* Larger form inputs */
|
|
581
|
+
input,
|
|
582
|
+
select,
|
|
583
|
+
textarea {
|
|
584
|
+
min-height: 48px;
|
|
585
|
+
font-size: 16px; /* Prevents iOS zoom on focus */
|
|
586
|
+
}
|
|
587
|
+
}
|
|
588
|
+
|
|
589
|
+
/* Hover only on devices that support it */
|
|
590
|
+
@media (hover: hover) {
|
|
591
|
+
.card:hover {
|
|
592
|
+
transform: translateY(-4px);
|
|
593
|
+
box-shadow: 0 12px 24px rgba(0, 0, 0, 0.15);
|
|
594
|
+
}
|
|
595
|
+
|
|
596
|
+
.button:hover {
|
|
597
|
+
background-color: var(--color-primary-dark);
|
|
598
|
+
}
|
|
599
|
+
}
|
|
600
|
+
|
|
601
|
+
/* Active states for touch devices */
|
|
602
|
+
@media (hover: none) {
|
|
603
|
+
.card:active {
|
|
604
|
+
transform: scale(0.98);
|
|
605
|
+
}
|
|
606
|
+
|
|
607
|
+
.button:active {
|
|
608
|
+
background-color: var(--color-primary-dark);
|
|
609
|
+
}
|
|
610
|
+
}
|
|
611
|
+
|
|
612
|
+
/* Disable hover effects on touch */
|
|
613
|
+
@media (hover: none) and (pointer: coarse) {
|
|
614
|
+
.hover-effect {
|
|
615
|
+
transform: none !important;
|
|
616
|
+
}
|
|
617
|
+
}
|
|
618
|
+
|
|
619
|
+
/* Safe area insets for notched devices */
|
|
620
|
+
.safe-area-padding {
|
|
621
|
+
padding-left: env(safe-area-inset-left);
|
|
622
|
+
padding-right: env(safe-area-inset-right);
|
|
623
|
+
padding-bottom: env(safe-area-inset-bottom);
|
|
624
|
+
}
|
|
625
|
+
|
|
626
|
+
.fixed-bottom-nav {
|
|
627
|
+
position: fixed;
|
|
628
|
+
bottom: 0;
|
|
629
|
+
left: 0;
|
|
630
|
+
right: 0;
|
|
631
|
+
padding-bottom: calc(1rem + env(safe-area-inset-bottom));
|
|
632
|
+
}
|
|
633
|
+
```
|
|
634
|
+
|
|
635
|
+
```tsx
|
|
636
|
+
// Touch-friendly button component
|
|
637
|
+
export function TouchButton({
|
|
638
|
+
children,
|
|
639
|
+
onClick,
|
|
640
|
+
...props
|
|
641
|
+
}: React.ButtonHTMLAttributes<HTMLButtonElement>) {
|
|
642
|
+
return (
|
|
643
|
+
<button
|
|
644
|
+
onClick={onClick}
|
|
645
|
+
className="
|
|
646
|
+
min-h-[44px] min-w-[44px]
|
|
647
|
+
px-4 py-2
|
|
648
|
+
touch-manipulation
|
|
649
|
+
select-none
|
|
650
|
+
active:scale-95
|
|
651
|
+
transition-transform duration-150
|
|
652
|
+
"
|
|
653
|
+
{...props}
|
|
654
|
+
>
|
|
655
|
+
{children}
|
|
656
|
+
</button>
|
|
657
|
+
);
|
|
658
|
+
}
|
|
659
|
+
|
|
660
|
+
// Swipe gesture hook
|
|
661
|
+
import { useState, useRef } from "react";
|
|
662
|
+
|
|
663
|
+
interface SwipeHandlers {
|
|
664
|
+
onSwipeLeft?: () => void;
|
|
665
|
+
onSwipeRight?: () => void;
|
|
666
|
+
onSwipeUp?: () => void;
|
|
667
|
+
onSwipeDown?: () => void;
|
|
668
|
+
}
|
|
669
|
+
|
|
670
|
+
export function useSwipe(handlers: SwipeHandlers, threshold = 50) {
|
|
671
|
+
const touchStart = useRef<{ x: number; y: number } | null>(null);
|
|
672
|
+
|
|
673
|
+
const onTouchStart = (e: React.TouchEvent) => {
|
|
674
|
+
touchStart.current = {
|
|
675
|
+
x: e.touches[0].clientX,
|
|
676
|
+
y: e.touches[0].clientY,
|
|
677
|
+
};
|
|
678
|
+
};
|
|
679
|
+
|
|
680
|
+
const onTouchEnd = (e: React.TouchEvent) => {
|
|
681
|
+
if (!touchStart.current) return;
|
|
682
|
+
|
|
683
|
+
const deltaX = e.changedTouches[0].clientX - touchStart.current.x;
|
|
684
|
+
const deltaY = e.changedTouches[0].clientY - touchStart.current.y;
|
|
685
|
+
|
|
686
|
+
if (Math.abs(deltaX) > Math.abs(deltaY)) {
|
|
687
|
+
if (deltaX > threshold) handlers.onSwipeRight?.();
|
|
688
|
+
if (deltaX < -threshold) handlers.onSwipeLeft?.();
|
|
689
|
+
} else {
|
|
690
|
+
if (deltaY > threshold) handlers.onSwipeDown?.();
|
|
691
|
+
if (deltaY < -threshold) handlers.onSwipeUp?.();
|
|
692
|
+
}
|
|
693
|
+
|
|
694
|
+
touchStart.current = null;
|
|
695
|
+
};
|
|
696
|
+
|
|
697
|
+
return { onTouchStart, onTouchEnd };
|
|
698
|
+
}
|
|
699
|
+
```
|
|
700
|
+
|
|
701
|
+
### 7. Responsive Navigation
|
|
702
|
+
|
|
703
|
+
```tsx
|
|
704
|
+
// Responsive navigation component
|
|
705
|
+
import { useState } from "react";
|
|
706
|
+
|
|
707
|
+
export function ResponsiveNav() {
|
|
708
|
+
const [isOpen, setIsOpen] = useState(false);
|
|
709
|
+
|
|
710
|
+
return (
|
|
711
|
+
<nav className="relative bg-white shadow-sm">
|
|
712
|
+
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
|
|
713
|
+
<div className="flex justify-between h-16">
|
|
714
|
+
{/* Logo */}
|
|
715
|
+
<div className="flex-shrink-0 flex items-center">
|
|
716
|
+
<Logo />
|
|
717
|
+
</div>
|
|
718
|
+
|
|
719
|
+
{/* Desktop navigation */}
|
|
720
|
+
<div className="hidden md:flex md:items-center md:space-x-8">
|
|
721
|
+
<NavLink href="/">Home</NavLink>
|
|
722
|
+
<NavLink href="/products">Products</NavLink>
|
|
723
|
+
<NavLink href="/about">About</NavLink>
|
|
724
|
+
<NavLink href="/contact">Contact</NavLink>
|
|
725
|
+
</div>
|
|
726
|
+
|
|
727
|
+
{/* Mobile menu button */}
|
|
728
|
+
<div className="flex items-center md:hidden">
|
|
729
|
+
<button
|
|
730
|
+
onClick={() => setIsOpen(!isOpen)}
|
|
731
|
+
className="
|
|
732
|
+
inline-flex items-center justify-center
|
|
733
|
+
p-2 rounded-md
|
|
734
|
+
text-gray-600 hover:text-gray-900 hover:bg-gray-100
|
|
735
|
+
focus:outline-none focus:ring-2 focus:ring-inset focus:ring-blue-500
|
|
736
|
+
"
|
|
737
|
+
aria-expanded={isOpen}
|
|
738
|
+
aria-controls="mobile-menu"
|
|
739
|
+
>
|
|
740
|
+
<span className="sr-only">
|
|
741
|
+
{isOpen ? "Close menu" : "Open menu"}
|
|
742
|
+
</span>
|
|
743
|
+
{isOpen ? (
|
|
744
|
+
<XIcon className="h-6 w-6" />
|
|
745
|
+
) : (
|
|
746
|
+
<MenuIcon className="h-6 w-6" />
|
|
747
|
+
)}
|
|
748
|
+
</button>
|
|
749
|
+
</div>
|
|
750
|
+
</div>
|
|
751
|
+
</div>
|
|
752
|
+
|
|
753
|
+
{/* Mobile menu */}
|
|
754
|
+
<div
|
|
755
|
+
id="mobile-menu"
|
|
756
|
+
className={`
|
|
757
|
+
md:hidden
|
|
758
|
+
${isOpen ? "block" : "hidden"}
|
|
759
|
+
`}
|
|
760
|
+
>
|
|
761
|
+
<div className="px-2 pt-2 pb-3 space-y-1 bg-white border-t">
|
|
762
|
+
<MobileNavLink href="/" onClick={() => setIsOpen(false)}>
|
|
763
|
+
Home
|
|
764
|
+
</MobileNavLink>
|
|
765
|
+
<MobileNavLink href="/products" onClick={() => setIsOpen(false)}>
|
|
766
|
+
Products
|
|
767
|
+
</MobileNavLink>
|
|
768
|
+
<MobileNavLink href="/about" onClick={() => setIsOpen(false)}>
|
|
769
|
+
About
|
|
770
|
+
</MobileNavLink>
|
|
771
|
+
<MobileNavLink href="/contact" onClick={() => setIsOpen(false)}>
|
|
772
|
+
Contact
|
|
773
|
+
</MobileNavLink>
|
|
774
|
+
</div>
|
|
775
|
+
</div>
|
|
776
|
+
</nav>
|
|
777
|
+
);
|
|
778
|
+
}
|
|
779
|
+
|
|
780
|
+
function MobileNavLink({
|
|
781
|
+
href,
|
|
782
|
+
children,
|
|
783
|
+
onClick,
|
|
784
|
+
}: {
|
|
785
|
+
href: string;
|
|
786
|
+
children: React.ReactNode;
|
|
787
|
+
onClick: () => void;
|
|
788
|
+
}) {
|
|
789
|
+
return (
|
|
790
|
+
<a
|
|
791
|
+
href={href}
|
|
792
|
+
onClick={onClick}
|
|
793
|
+
className="
|
|
794
|
+
block px-3 py-2 rounded-md
|
|
795
|
+
text-base font-medium
|
|
796
|
+
text-gray-700 hover:text-gray-900 hover:bg-gray-50
|
|
797
|
+
min-h-[44px] flex items-center
|
|
798
|
+
"
|
|
799
|
+
>
|
|
800
|
+
{children}
|
|
801
|
+
</a>
|
|
802
|
+
);
|
|
803
|
+
}
|
|
804
|
+
```
|
|
805
|
+
|
|
806
|
+
## Use Cases
|
|
807
|
+
|
|
808
|
+
### Responsive Card Layout
|
|
809
|
+
|
|
810
|
+
```tsx
|
|
811
|
+
export function ResponsiveCardGrid() {
|
|
812
|
+
return (
|
|
813
|
+
<div
|
|
814
|
+
className="
|
|
815
|
+
grid gap-4 sm:gap-6
|
|
816
|
+
grid-cols-1
|
|
817
|
+
sm:grid-cols-2
|
|
818
|
+
lg:grid-cols-3
|
|
819
|
+
xl:grid-cols-4
|
|
820
|
+
"
|
|
821
|
+
>
|
|
822
|
+
{cards.map((card) => (
|
|
823
|
+
<Card key={card.id} className="h-full">
|
|
824
|
+
<CardImage src={card.image} alt={card.title} />
|
|
825
|
+
<CardContent>
|
|
826
|
+
<CardTitle className="line-clamp-2">{card.title}</CardTitle>
|
|
827
|
+
<CardDescription className="line-clamp-3">
|
|
828
|
+
{card.description}
|
|
829
|
+
</CardDescription>
|
|
830
|
+
</CardContent>
|
|
831
|
+
</Card>
|
|
832
|
+
))}
|
|
833
|
+
</div>
|
|
834
|
+
);
|
|
39
835
|
}
|
|
40
836
|
```
|
|
41
837
|
|
|
42
838
|
## Best Practices
|
|
43
|
-
|
|
44
|
-
|
|
839
|
+
|
|
840
|
+
### Do's
|
|
841
|
+
|
|
842
|
+
- Start with mobile-first CSS
|
|
843
|
+
- Use relative units (rem, em, %)
|
|
45
844
|
- Test on real devices
|
|
46
|
-
-
|
|
845
|
+
- Use semantic HTML
|
|
846
|
+
- Implement touch-friendly targets
|
|
847
|
+
- Consider reduced motion preferences
|
|
848
|
+
- Use container queries for components
|
|
849
|
+
- Optimize images for different sizes
|
|
850
|
+
- Test across browsers
|
|
851
|
+
- Use CSS logical properties
|
|
852
|
+
|
|
853
|
+
### Don'ts
|
|
854
|
+
|
|
855
|
+
- Don't hide content on mobile unnecessarily
|
|
856
|
+
- Don't use fixed widths
|
|
857
|
+
- Don't rely only on hover states
|
|
858
|
+
- Don't use small touch targets
|
|
859
|
+
- Don't ignore landscape orientation
|
|
860
|
+
- Don't skip accessibility testing
|
|
861
|
+
- Don't use device-specific breakpoints
|
|
862
|
+
- Don't forget keyboard navigation
|
|
863
|
+
- Don't ignore safe area insets
|
|
864
|
+
- Don't assume mouse input
|
|
865
|
+
|
|
866
|
+
## References
|
|
867
|
+
|
|
868
|
+
- [CSS Media Queries](https://developer.mozilla.org/en-US/docs/Web/CSS/Media_Queries)
|
|
869
|
+
- [Container Queries](https://developer.mozilla.org/en-US/docs/Web/CSS/CSS_Container_Queries)
|
|
870
|
+
- [Responsive Images](https://developer.mozilla.org/en-US/docs/Learn/HTML/Multimedia_and_embedding/Responsive_images)
|
|
871
|
+
- [Touch Events](https://developer.mozilla.org/en-US/docs/Web/API/Touch_events)
|
|
872
|
+
- [Viewport Concepts](https://developer.mozilla.org/en-US/docs/Web/CSS/Viewport_concepts)
|