omgkit 2.2.0 → 2.3.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/databases/mongodb/SKILL.md +60 -776
- package/plugin/skills/databases/prisma/SKILL.md +53 -744
- package/plugin/skills/databases/redis/SKILL.md +53 -860
- package/plugin/skills/devops/aws/SKILL.md +68 -672
- package/plugin/skills/devops/github-actions/SKILL.md +54 -657
- package/plugin/skills/devops/kubernetes/SKILL.md +67 -602
- package/plugin/skills/devops/performance-profiling/SKILL.md +59 -863
- package/plugin/skills/frameworks/django/SKILL.md +87 -853
- package/plugin/skills/frameworks/express/SKILL.md +95 -1301
- package/plugin/skills/frameworks/fastapi/SKILL.md +90 -1198
- package/plugin/skills/frameworks/laravel/SKILL.md +87 -1187
- package/plugin/skills/frameworks/nestjs/SKILL.md +106 -973
- package/plugin/skills/frameworks/react/SKILL.md +94 -962
- package/plugin/skills/frameworks/vue/SKILL.md +95 -1242
- package/plugin/skills/frontend/accessibility/SKILL.md +91 -1056
- package/plugin/skills/frontend/frontend-design/SKILL.md +69 -1262
- package/plugin/skills/frontend/responsive/SKILL.md +76 -799
- package/plugin/skills/frontend/shadcn-ui/SKILL.md +73 -921
- package/plugin/skills/frontend/tailwindcss/SKILL.md +60 -788
- package/plugin/skills/frontend/threejs/SKILL.md +72 -1266
- package/plugin/skills/languages/javascript/SKILL.md +106 -849
- package/plugin/skills/methodology/brainstorming/SKILL.md +70 -576
- package/plugin/skills/methodology/defense-in-depth/SKILL.md +79 -831
- package/plugin/skills/methodology/dispatching-parallel-agents/SKILL.md +81 -654
- package/plugin/skills/methodology/executing-plans/SKILL.md +86 -529
- package/plugin/skills/methodology/finishing-development-branch/SKILL.md +95 -586
- package/plugin/skills/methodology/problem-solving/SKILL.md +67 -681
- package/plugin/skills/methodology/receiving-code-review/SKILL.md +70 -533
- package/plugin/skills/methodology/requesting-code-review/SKILL.md +70 -610
- package/plugin/skills/methodology/root-cause-tracing/SKILL.md +70 -646
- package/plugin/skills/methodology/sequential-thinking/SKILL.md +70 -478
- package/plugin/skills/methodology/systematic-debugging/SKILL.md +66 -559
- package/plugin/skills/methodology/test-driven-development/SKILL.md +91 -752
- package/plugin/skills/methodology/testing-anti-patterns/SKILL.md +78 -687
- package/plugin/skills/methodology/token-optimization/SKILL.md +72 -602
- package/plugin/skills/methodology/verification-before-completion/SKILL.md +108 -529
- package/plugin/skills/methodology/writing-plans/SKILL.md +79 -566
- package/plugin/skills/omega/omega-architecture/SKILL.md +91 -752
- package/plugin/skills/omega/omega-coding/SKILL.md +161 -552
- package/plugin/skills/omega/omega-sprint/SKILL.md +132 -777
- package/plugin/skills/omega/omega-testing/SKILL.md +157 -845
- package/plugin/skills/omega/omega-thinking/SKILL.md +165 -606
- package/plugin/skills/security/better-auth/SKILL.md +46 -1034
- package/plugin/skills/security/oauth/SKILL.md +80 -934
- package/plugin/skills/security/owasp/SKILL.md +78 -862
- package/plugin/skills/testing/playwright/SKILL.md +77 -700
- package/plugin/skills/testing/pytest/SKILL.md +73 -811
- package/plugin/skills/testing/vitest/SKILL.md +60 -920
- package/plugin/skills/tools/document-processing/SKILL.md +111 -838
- package/plugin/skills/tools/image-processing/SKILL.md +126 -659
- package/plugin/skills/tools/mcp-development/SKILL.md +85 -758
- package/plugin/skills/tools/media-processing/SKILL.md +118 -735
- package/plugin/stdrules/SKILL_STANDARDS.md +490 -0
- package/plugin/skills/SKILL_STANDARDS.md +0 -743
|
@@ -1,872 +1,149 @@
|
|
|
1
1
|
---
|
|
2
|
-
name: responsive
|
|
3
|
-
description:
|
|
4
|
-
category: frontend
|
|
5
|
-
triggers:
|
|
6
|
-
- responsive
|
|
7
|
-
- responsive design
|
|
8
|
-
- mobile first
|
|
9
|
-
- adaptive layout
|
|
10
|
-
- breakpoints
|
|
11
|
-
- media queries
|
|
2
|
+
name: building-responsive-layouts
|
|
3
|
+
description: Claude builds responsive web layouts with mobile-first CSS, fluid typography, and container queries. Use when creating adaptive UIs that work across all device sizes.
|
|
12
4
|
---
|
|
13
5
|
|
|
14
|
-
# Responsive
|
|
6
|
+
# Building Responsive Layouts
|
|
15
7
|
|
|
16
|
-
|
|
8
|
+
## Quick Start
|
|
17
9
|
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
## Features
|
|
31
|
-
|
|
32
|
-
### 1. Mobile-First Breakpoints
|
|
33
|
-
|
|
34
|
-
```css
|
|
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
|
-
}
|
|
78
|
-
|
|
79
|
-
/* Extra large devices (1280px and up) */
|
|
80
|
-
@media (min-width: 1280px) {
|
|
81
|
-
.container {
|
|
82
|
-
max-width: 1280px;
|
|
83
|
-
}
|
|
10
|
+
```tsx
|
|
11
|
+
// Responsive grid with auto-fit
|
|
12
|
+
export function ResponsiveGrid({ children, minWidth = '300px' }: GridProps) {
|
|
13
|
+
return (
|
|
14
|
+
<div style={{
|
|
15
|
+
display: 'grid',
|
|
16
|
+
gridTemplateColumns: `repeat(auto-fit, minmax(min(${minWidth}, 100%), 1fr))`,
|
|
17
|
+
gap: '1.5rem'
|
|
18
|
+
}}>
|
|
19
|
+
{children}
|
|
20
|
+
</div>
|
|
21
|
+
);
|
|
84
22
|
}
|
|
85
23
|
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
.
|
|
89
|
-
|
|
90
|
-
}
|
|
91
|
-
}
|
|
24
|
+
// With Tailwind
|
|
25
|
+
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4 gap-4 md:gap-6">
|
|
26
|
+
{items.map(item => <Card key={item.id} {...item} />)}
|
|
27
|
+
</div>
|
|
92
28
|
```
|
|
93
29
|
|
|
94
|
-
|
|
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
|
-
}, []);
|
|
30
|
+
## Features
|
|
129
31
|
|
|
130
|
-
|
|
131
|
-
|
|
32
|
+
| Feature | Description | Guide |
|
|
33
|
+
|---------|-------------|-------|
|
|
34
|
+
| Mobile-First Breakpoints | sm(640), md(768), lg(1024), xl(1280), 2xl(1536) | `ref/breakpoints.md` |
|
|
35
|
+
| Fluid Typography | `clamp()` for responsive font sizes | `ref/fluid-type.md` |
|
|
36
|
+
| Container Queries | Component-level responsive design | `ref/container-queries.md` |
|
|
37
|
+
| Responsive Images | srcset, sizes, and art direction | `ref/images.md` |
|
|
38
|
+
| Touch-Friendly | 44px minimum targets, hover vs touch handling | `ref/touch.md` |
|
|
39
|
+
| Safe Areas | Handle notched devices and dynamic viewports | `ref/safe-areas.md` |
|
|
132
40
|
|
|
133
|
-
|
|
134
|
-
}
|
|
41
|
+
## Common Patterns
|
|
135
42
|
|
|
136
|
-
|
|
137
|
-
import { useState, useEffect } from "react";
|
|
43
|
+
### useMediaQuery Hook
|
|
138
44
|
|
|
45
|
+
```tsx
|
|
139
46
|
export function useMediaQuery(query: string): boolean {
|
|
140
47
|
const [matches, setMatches] = useState(false);
|
|
141
48
|
|
|
142
49
|
useEffect(() => {
|
|
143
50
|
const media = window.matchMedia(query);
|
|
144
|
-
|
|
145
|
-
setMatches(media.matches);
|
|
146
|
-
}
|
|
147
|
-
|
|
51
|
+
setMatches(media.matches);
|
|
148
52
|
const listener = (e: MediaQueryListEvent) => setMatches(e.matches);
|
|
149
|
-
media.addEventListener(
|
|
150
|
-
return () => media.removeEventListener(
|
|
151
|
-
}, [
|
|
53
|
+
media.addEventListener('change', listener);
|
|
54
|
+
return () => media.removeEventListener('change', listener);
|
|
55
|
+
}, [query]);
|
|
152
56
|
|
|
153
57
|
return matches;
|
|
154
58
|
}
|
|
155
59
|
|
|
156
60
|
// Usage
|
|
157
|
-
|
|
158
|
-
|
|
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
|
|
175
|
-
|
|
176
|
-
```css
|
|
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
|
-
}
|
|
61
|
+
const isMobile = useMediaQuery('(max-width: 639px)');
|
|
62
|
+
const prefersReducedMotion = useMediaQuery('(prefers-reduced-motion: reduce)');
|
|
353
63
|
```
|
|
354
64
|
|
|
355
|
-
###
|
|
65
|
+
### Container Query Component
|
|
356
66
|
|
|
357
67
|
```css
|
|
358
|
-
/* Container query setup */
|
|
359
68
|
.card-container {
|
|
360
69
|
container-type: inline-size;
|
|
361
|
-
container-name: card;
|
|
362
70
|
}
|
|
363
71
|
|
|
364
|
-
/* Base card styles (smallest size) */
|
|
365
72
|
.card {
|
|
366
73
|
display: flex;
|
|
367
74
|
flex-direction: column;
|
|
368
|
-
padding: 1rem;
|
|
369
75
|
}
|
|
370
76
|
|
|
371
|
-
|
|
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) {
|
|
77
|
+
@container (min-width: 400px) {
|
|
383
78
|
.card {
|
|
384
79
|
flex-direction: row;
|
|
385
80
|
gap: 1rem;
|
|
386
81
|
}
|
|
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
|
-
}
|
|
82
|
+
.card-image { width: 40%; }
|
|
427
83
|
}
|
|
428
84
|
```
|
|
429
85
|
|
|
430
86
|
```tsx
|
|
431
|
-
|
|
432
|
-
export function ContainerQueryCard({ title, description, image }: CardProps) {
|
|
87
|
+
export function ResponsiveCard({ image, title, content }: CardProps) {
|
|
433
88
|
return (
|
|
434
89
|
<div className="card-container">
|
|
435
90
|
<article className="card">
|
|
436
91
|
<img src={image} alt="" className="card-image" />
|
|
437
92
|
<div className="card-content">
|
|
438
|
-
<h3
|
|
439
|
-
<p
|
|
93
|
+
<h3>{title}</h3>
|
|
94
|
+
<p>{content}</p>
|
|
440
95
|
</div>
|
|
441
96
|
</article>
|
|
442
97
|
</div>
|
|
443
98
|
);
|
|
444
99
|
}
|
|
445
|
-
|
|
446
|
-
// CSS Module with container queries
|
|
447
|
-
// Card.module.css
|
|
448
|
-
/*
|
|
449
|
-
.container {
|
|
450
|
-
container-type: inline-size;
|
|
451
|
-
}
|
|
452
|
-
|
|
453
|
-
.card {
|
|
454
|
-
display: grid;
|
|
455
|
-
gap: 1rem;
|
|
456
|
-
}
|
|
457
|
-
|
|
458
|
-
@container (min-width: 30rem) {
|
|
459
|
-
.card {
|
|
460
|
-
grid-template-columns: 1fr 2fr;
|
|
461
|
-
}
|
|
462
|
-
}
|
|
463
|
-
*/
|
|
464
|
-
```
|
|
465
|
-
|
|
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
|
-
|
|
556
|
-
```css
|
|
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
100
|
```
|
|
700
101
|
|
|
701
|
-
###
|
|
102
|
+
### Responsive Navigation
|
|
702
103
|
|
|
703
104
|
```tsx
|
|
704
|
-
// Responsive navigation component
|
|
705
|
-
import { useState } from "react";
|
|
706
|
-
|
|
707
105
|
export function ResponsiveNav() {
|
|
708
106
|
const [isOpen, setIsOpen] = useState(false);
|
|
709
107
|
|
|
710
108
|
return (
|
|
711
|
-
<nav className="relative
|
|
712
|
-
<div className="
|
|
713
|
-
<
|
|
714
|
-
|
|
715
|
-
|
|
716
|
-
|
|
717
|
-
</
|
|
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>
|
|
109
|
+
<nav className="relative">
|
|
110
|
+
<div className="flex justify-between items-center h-16 px-4">
|
|
111
|
+
<Logo />
|
|
112
|
+
{/* Desktop nav */}
|
|
113
|
+
<div className="hidden md:flex items-center space-x-8">
|
|
114
|
+
<NavLink href="/">Home</NavLink>
|
|
115
|
+
<NavLink href="/about">About</NavLink>
|
|
750
116
|
</div>
|
|
117
|
+
{/* Mobile menu button */}
|
|
118
|
+
<button
|
|
119
|
+
className="md:hidden p-2 min-h-[44px] min-w-[44px]"
|
|
120
|
+
onClick={() => setIsOpen(!isOpen)}
|
|
121
|
+
aria-expanded={isOpen}
|
|
122
|
+
>
|
|
123
|
+
<span className="sr-only">{isOpen ? 'Close' : 'Open'} menu</span>
|
|
124
|
+
{isOpen ? <XIcon /> : <MenuIcon />}
|
|
125
|
+
</button>
|
|
751
126
|
</div>
|
|
752
|
-
|
|
753
|
-
{
|
|
754
|
-
|
|
755
|
-
|
|
756
|
-
|
|
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>
|
|
127
|
+
{/* Mobile nav */}
|
|
128
|
+
{isOpen && (
|
|
129
|
+
<div className="md:hidden px-2 pb-3 space-y-1">
|
|
130
|
+
<MobileNavLink href="/" onClick={() => setIsOpen(false)}>Home</MobileNavLink>
|
|
131
|
+
<MobileNavLink href="/about" onClick={() => setIsOpen(false)}>About</MobileNavLink>
|
|
774
132
|
</div>
|
|
775
|
-
|
|
133
|
+
)}
|
|
776
134
|
</nav>
|
|
777
135
|
);
|
|
778
136
|
}
|
|
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
|
-
);
|
|
835
|
-
}
|
|
836
137
|
```
|
|
837
138
|
|
|
838
139
|
## Best Practices
|
|
839
140
|
|
|
840
|
-
|
|
841
|
-
|
|
842
|
-
|
|
843
|
-
|
|
844
|
-
|
|
845
|
-
|
|
846
|
-
|
|
847
|
-
|
|
848
|
-
-
|
|
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)
|
|
141
|
+
| Do | Avoid |
|
|
142
|
+
|----|-------|
|
|
143
|
+
| Start with mobile-first CSS | Hiding essential content on mobile |
|
|
144
|
+
| Use relative units (rem, %, vw) | Fixed pixel widths |
|
|
145
|
+
| Test on real devices | Relying only on hover states |
|
|
146
|
+
| Use 44px minimum touch targets | Small touch targets on mobile |
|
|
147
|
+
| Consider reduced motion preferences | Ignoring landscape orientation |
|
|
148
|
+
| Use CSS logical properties (inline, block) | Device-specific breakpoints |
|
|
149
|
+
| Handle safe-area-inset for notched devices | Assuming mouse input |
|