picasso-skill 1.5.1 → 2.0.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/agents/picasso.md +14 -2
- package/checklists/pre-ship.md +83 -0
- package/commands/backlog.md +34 -0
- package/commands/variants.md +18 -0
- package/package.json +3 -1
- package/references/accessibility-wcag.md +245 -0
- package/references/anti-patterns.md +184 -0
- package/references/color-and-contrast.md +477 -0
- package/references/component-patterns.md +113 -0
- package/references/conversion-design.md +193 -0
- package/references/data-visualization.md +226 -0
- package/references/depth-and-elevation.md +211 -0
- package/references/design-system.md +176 -0
- package/references/generative-art.md +648 -0
- package/references/interaction-design.md +162 -0
- package/references/modern-css-performance.md +361 -0
- package/references/motion-and-animation.md +267 -0
- package/references/performance-optimization.md +746 -0
- package/references/react-patterns.md +318 -0
- package/references/responsive-design.md +452 -0
- package/references/sensory-design.md +369 -0
- package/references/spatial-design.md +176 -0
- package/references/style-presets.md +502 -0
- package/references/tools-catalog.md +103 -0
- package/references/typography.md +415 -0
- package/references/ux-psychology.md +235 -0
- package/references/ux-writing.md +513 -0
- package/skills/picasso/SKILL.md +58 -2
- package/skills/picasso/references/animation-performance.md +244 -0
- package/skills/picasso/references/brand-and-identity.md +136 -0
- package/skills/picasso/references/code-typography.md +222 -0
- package/skills/picasso/references/color-and-contrast.md +56 -2
- package/skills/picasso/references/dark-mode.md +199 -0
- package/skills/picasso/references/depth-and-elevation.md +211 -0
- package/skills/picasso/references/i18n-visual-patterns.md +177 -0
- package/skills/picasso/references/images-and-media.md +222 -0
- package/skills/picasso/references/loading-and-states.md +258 -0
- package/skills/picasso/references/micro-interactions.md +291 -0
- package/skills/picasso/references/motion-and-animation.md +9 -2
- package/skills/picasso/references/navigation-patterns.md +247 -0
- package/skills/picasso/references/style-presets.md +1 -1
- package/skills/picasso/references/tables-and-forms.md +227 -0
- package/skills/picasso/references/tools-catalog.md +103 -0
- package/skills/picasso/references/typography.md +45 -2
|
@@ -0,0 +1,369 @@
|
|
|
1
|
+
# Sensory Design Reference
|
|
2
|
+
|
|
3
|
+
## Table of Contents
|
|
4
|
+
1. UI Sound Design
|
|
5
|
+
2. Haptic Feedback
|
|
6
|
+
3. Multi-Sensory Integration
|
|
7
|
+
|
|
8
|
+
---
|
|
9
|
+
|
|
10
|
+
## 1. UI Sound Design
|
|
11
|
+
|
|
12
|
+
### Why Sound
|
|
13
|
+
Sound provides confirmation that an action occurred, draws attention to important state changes, and adds personality to the interface. It is underused on the web but highly effective when applied with restraint.
|
|
14
|
+
|
|
15
|
+
### Sourcing Sounds
|
|
16
|
+
- **Kenney.nl** -- free CC0 game/UI sound packs. High quality, no attribution required.
|
|
17
|
+
- **Freesound.org** -- filter by CC0 license for attribution-free use. Verify license on each clip.
|
|
18
|
+
- **Tone.js synthesis** -- generate procedural sounds at runtime. No files to load. Good for simple tones, chimes, and click sounds. Use `Tone.Synth`, `Tone.MembraneSynth`, or `Tone.NoiseSynth` for UI feedback.
|
|
19
|
+
- **Self-recorded** -- record short foley sounds and export as WAV/MP3 at 44.1kHz mono.
|
|
20
|
+
|
|
21
|
+
For inline embedding, convert sounds to base64 data URIs to avoid CORS issues and runtime fetching. Each sound becomes a self-contained TypeScript module.
|
|
22
|
+
|
|
23
|
+
```typescript
|
|
24
|
+
// sounds/click-soft.ts
|
|
25
|
+
export const clickSoftSound = "data:audio/wav;base64,UklGR...";
|
|
26
|
+
```
|
|
27
|
+
|
|
28
|
+
### The useSound Hook (Production-Ready)
|
|
29
|
+
|
|
30
|
+
This hook handles AudioContext lifecycle (including the browser autoplay policy that blocks AudioContext until a user gesture), caches decoded buffers so each sound is fetched and decoded only once, and exposes volume control via a GainNode.
|
|
31
|
+
|
|
32
|
+
```typescript
|
|
33
|
+
// hooks/use-sound.ts
|
|
34
|
+
import { useCallback, useEffect, useRef } from "react";
|
|
35
|
+
|
|
36
|
+
// Shared AudioContext singleton -- one per page avoids resource waste.
|
|
37
|
+
let sharedCtx: AudioContext | null = null;
|
|
38
|
+
const bufferCache = new Map<string, AudioBuffer>();
|
|
39
|
+
|
|
40
|
+
function getAudioContext(): AudioContext {
|
|
41
|
+
if (!sharedCtx) {
|
|
42
|
+
sharedCtx = new AudioContext();
|
|
43
|
+
}
|
|
44
|
+
return sharedCtx;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
// Resume AudioContext on first user gesture (required by Chrome, Safari, Firefox).
|
|
48
|
+
// Call this once at app root.
|
|
49
|
+
export function initAudioOnGesture(): void {
|
|
50
|
+
const resume = () => {
|
|
51
|
+
const ctx = getAudioContext();
|
|
52
|
+
if (ctx.state === "suspended") {
|
|
53
|
+
ctx.resume();
|
|
54
|
+
}
|
|
55
|
+
};
|
|
56
|
+
const events = ["click", "touchstart", "keydown"] as const;
|
|
57
|
+
events.forEach((e) => document.addEventListener(e, resume, { once: false }));
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
interface UseSoundOptions {
|
|
61
|
+
volume?: number; // 0 to 1, default 0.4
|
|
62
|
+
/** For sound sprites: start offset in seconds */
|
|
63
|
+
offset?: number;
|
|
64
|
+
/** For sound sprites: duration in seconds */
|
|
65
|
+
duration?: number;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
export function useSound(src: string, options: UseSoundOptions = {}) {
|
|
69
|
+
const { volume = 0.4, offset, duration } = options;
|
|
70
|
+
const gainRef = useRef<GainNode | null>(null);
|
|
71
|
+
|
|
72
|
+
const play = useCallback(async () => {
|
|
73
|
+
const ctx = getAudioContext();
|
|
74
|
+
|
|
75
|
+
// Resume if suspended (covers edge cases where gesture init missed)
|
|
76
|
+
if (ctx.state === "suspended") {
|
|
77
|
+
await ctx.resume();
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
// Fetch + decode only on first play, then cache
|
|
81
|
+
let buffer = bufferCache.get(src);
|
|
82
|
+
if (!buffer) {
|
|
83
|
+
const response = await fetch(src);
|
|
84
|
+
const arrayBuf = await response.arrayBuffer();
|
|
85
|
+
buffer = await ctx.decodeAudioData(arrayBuf);
|
|
86
|
+
bufferCache.set(src, buffer);
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
// GainNode for volume control
|
|
90
|
+
const gain = ctx.createGain();
|
|
91
|
+
gain.gain.value = volume;
|
|
92
|
+
gain.connect(ctx.destination);
|
|
93
|
+
gainRef.current = gain;
|
|
94
|
+
|
|
95
|
+
const source = ctx.createBufferSource();
|
|
96
|
+
source.buffer = buffer;
|
|
97
|
+
source.connect(gain);
|
|
98
|
+
|
|
99
|
+
if (offset !== undefined && duration !== undefined) {
|
|
100
|
+
source.start(0, offset, duration);
|
|
101
|
+
} else {
|
|
102
|
+
source.start(0);
|
|
103
|
+
}
|
|
104
|
+
}, [src, volume, offset, duration]);
|
|
105
|
+
|
|
106
|
+
const setVolume = useCallback(
|
|
107
|
+
(v: number) => {
|
|
108
|
+
if (gainRef.current) {
|
|
109
|
+
gainRef.current.gain.value = Math.max(0, Math.min(1, v));
|
|
110
|
+
}
|
|
111
|
+
},
|
|
112
|
+
[]
|
|
113
|
+
);
|
|
114
|
+
|
|
115
|
+
return { play, setVolume } as const;
|
|
116
|
+
}
|
|
117
|
+
```
|
|
118
|
+
|
|
119
|
+
### App Root Setup
|
|
120
|
+
|
|
121
|
+
```tsx
|
|
122
|
+
// app/layout.tsx or app entry
|
|
123
|
+
import { initAudioOnGesture } from "@/hooks/use-sound";
|
|
124
|
+
|
|
125
|
+
// Call once on mount
|
|
126
|
+
useEffect(() => {
|
|
127
|
+
initAudioOnGesture();
|
|
128
|
+
}, []);
|
|
129
|
+
```
|
|
130
|
+
|
|
131
|
+
### Basic Usage
|
|
132
|
+
|
|
133
|
+
```tsx
|
|
134
|
+
import { useSound } from "@/hooks/use-sound";
|
|
135
|
+
import { clickSoftSound } from "@/sounds/click-soft";
|
|
136
|
+
|
|
137
|
+
function SaveButton() {
|
|
138
|
+
const { play } = useSound(clickSoftSound, { volume: 0.3 });
|
|
139
|
+
return (
|
|
140
|
+
<button onClick={() => { play(); handleSave(); }}>
|
|
141
|
+
Save
|
|
142
|
+
</button>
|
|
143
|
+
);
|
|
144
|
+
}
|
|
145
|
+
```
|
|
146
|
+
|
|
147
|
+
### Sound Sprite Pattern
|
|
148
|
+
|
|
149
|
+
Pack multiple short sounds into a single audio file to reduce HTTP requests and simplify asset management. Reference each sound by its offset and duration within the file.
|
|
150
|
+
|
|
151
|
+
```typescript
|
|
152
|
+
// sounds/ui-sprite.ts
|
|
153
|
+
export const uiSprite = "data:audio/wav;base64,UklGR...";
|
|
154
|
+
|
|
155
|
+
// Offsets in seconds within the sprite file
|
|
156
|
+
export const SPRITE_MAP = {
|
|
157
|
+
click: { offset: 0.0, duration: 0.08 },
|
|
158
|
+
success: { offset: 0.1, duration: 0.15 },
|
|
159
|
+
error: { offset: 0.3, duration: 0.12 },
|
|
160
|
+
toggle: { offset: 0.5, duration: 0.06 },
|
|
161
|
+
whoosh: { offset: 0.6, duration: 0.20 },
|
|
162
|
+
} as const;
|
|
163
|
+
```
|
|
164
|
+
|
|
165
|
+
```tsx
|
|
166
|
+
import { useSound } from "@/hooks/use-sound";
|
|
167
|
+
import { uiSprite, SPRITE_MAP } from "@/sounds/ui-sprite";
|
|
168
|
+
|
|
169
|
+
function ToggleSwitch({ checked, onChange }: ToggleProps) {
|
|
170
|
+
const { play } = useSound(uiSprite, {
|
|
171
|
+
volume: 0.35,
|
|
172
|
+
offset: SPRITE_MAP.toggle.offset,
|
|
173
|
+
duration: SPRITE_MAP.toggle.duration,
|
|
174
|
+
});
|
|
175
|
+
|
|
176
|
+
return (
|
|
177
|
+
<button
|
|
178
|
+
role="switch"
|
|
179
|
+
aria-checked={checked}
|
|
180
|
+
onClick={() => { play(); onChange(!checked); }}
|
|
181
|
+
/>
|
|
182
|
+
);
|
|
183
|
+
}
|
|
184
|
+
```
|
|
185
|
+
|
|
186
|
+
### When to Use Sound
|
|
187
|
+
- **Button clicks**: soft, short click sound (50-100ms)
|
|
188
|
+
- **Success actions**: pleasant confirmation tone
|
|
189
|
+
- **Notifications**: attention-getting but not alarming chime
|
|
190
|
+
- **Errors**: subtle alert, not a harsh buzz
|
|
191
|
+
- **Toggle switches**: satisfying mechanical click
|
|
192
|
+
- **Transitions**: whoosh or swipe sound for page changes
|
|
193
|
+
|
|
194
|
+
### Rules
|
|
195
|
+
- Always provide a sound toggle in the UI (respect user preference)
|
|
196
|
+
- Keep sounds under 200ms for UI interactions
|
|
197
|
+
- Use the Web Audio API, not `<audio>` elements (lower latency)
|
|
198
|
+
- Sound volume should be subtle by default (0.3-0.5 of max)
|
|
199
|
+
- Never auto-play sounds on page load
|
|
200
|
+
- Gate behind a user preference stored in localStorage or app settings
|
|
201
|
+
- Pre-decode buffers when possible; never decode on every play
|
|
202
|
+
|
|
203
|
+
---
|
|
204
|
+
|
|
205
|
+
## 2. Haptic Feedback
|
|
206
|
+
|
|
207
|
+
### The Vibration API (Android, some desktop browsers)
|
|
208
|
+
|
|
209
|
+
```typescript
|
|
210
|
+
// utils/haptics.ts
|
|
211
|
+
export function hapticTap() {
|
|
212
|
+
navigator.vibrate?.(10);
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
export function hapticSuccess() {
|
|
216
|
+
navigator.vibrate?.([10, 50, 10]);
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
export function hapticError() {
|
|
220
|
+
navigator.vibrate?.(30);
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
export function hapticWarning() {
|
|
224
|
+
navigator.vibrate?.([10, 30, 10, 30, 10]);
|
|
225
|
+
}
|
|
226
|
+
```
|
|
227
|
+
|
|
228
|
+
### iOS Haptic Considerations
|
|
229
|
+
|
|
230
|
+
The Vibration API is not supported on iOS Safari. For web apps, iOS has no standard haptic API. Strategies for iOS haptic feedback:
|
|
231
|
+
|
|
232
|
+
1. **Native bridge (Capacitor/React Native)** -- If the app runs in a native wrapper, call `UIImpactFeedbackGenerator` through the bridge. This provides the best haptics on iOS with three intensity levels: `.light`, `.medium`, `.heavy`.
|
|
233
|
+
|
|
234
|
+
```typescript
|
|
235
|
+
// For Capacitor-based apps
|
|
236
|
+
import { Haptics, ImpactStyle } from "@capacitor/haptics";
|
|
237
|
+
|
|
238
|
+
export async function hapticTapIOS() {
|
|
239
|
+
await Haptics.impact({ style: ImpactStyle.Light });
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
export async function hapticSuccessIOS() {
|
|
243
|
+
await Haptics.notification({ type: "success" });
|
|
244
|
+
}
|
|
245
|
+
```
|
|
246
|
+
|
|
247
|
+
2. **Graceful degradation** -- For pure web apps on iOS, accept that haptics are unavailable. Never show broken behavior. Wrap all haptic calls behind a safe helper:
|
|
248
|
+
|
|
249
|
+
```typescript
|
|
250
|
+
export function haptic(pattern: number | number[]) {
|
|
251
|
+
if (typeof navigator !== "undefined" && "vibrate" in navigator) {
|
|
252
|
+
navigator.vibrate(pattern);
|
|
253
|
+
}
|
|
254
|
+
// On iOS Safari this is a no-op. The visual and audio
|
|
255
|
+
// feedback channels carry the interaction.
|
|
256
|
+
}
|
|
257
|
+
```
|
|
258
|
+
|
|
259
|
+
### When to Use Haptics
|
|
260
|
+
- **Button press confirmation**: 10ms pulse on touch
|
|
261
|
+
- **Toggle switch**: 10ms pulse on state change
|
|
262
|
+
- **Destructive action confirmation**: 30ms pulse before confirmation dialog
|
|
263
|
+
- **Pull-to-refresh threshold**: 10ms pulse when the threshold is reached
|
|
264
|
+
- **Drag and drop**: 10ms pulse on pickup and drop
|
|
265
|
+
|
|
266
|
+
### Rules
|
|
267
|
+
- Gate behind feature detection (`'vibrate' in navigator` or Capacitor availability)
|
|
268
|
+
- Respect `prefers-reduced-motion` by disabling haptics when motion is reduced
|
|
269
|
+
- Keep durations very short (10-30ms for taps, never longer than 100ms)
|
|
270
|
+
- Do not use haptics for every interaction, only pivotal moments
|
|
271
|
+
- Test on real hardware; emulators do not produce haptic feedback
|
|
272
|
+
|
|
273
|
+
---
|
|
274
|
+
|
|
275
|
+
## 3. Multi-Sensory Integration
|
|
276
|
+
|
|
277
|
+
The strongest UI moments combine visual, auditory, and haptic feedback simultaneously. This triple feedback creates a moment of certainty that a single visual change cannot match. Reserve it for milestone moments: order placed, task completed, level achieved, payment confirmed.
|
|
278
|
+
|
|
279
|
+
### Integration Principles
|
|
280
|
+
1. **Synchronize channels** -- sound, haptic, and animation should fire at the same instant. Do not stagger them.
|
|
281
|
+
2. **Match intensity** -- a subtle visual pulse pairs with a quiet click and a light tap. A big celebration pairs with a brighter chime and stronger vibration.
|
|
282
|
+
3. **Degrade gracefully** -- if sound is muted, the visual and haptic still work. If haptics are unavailable, sound and visual carry it. Each channel must stand on its own.
|
|
283
|
+
4. **Respect preferences** -- check `prefers-reduced-motion`, sound toggle state, and haptic toggle state independently.
|
|
284
|
+
|
|
285
|
+
### Complete Example: Task Completion Feedback
|
|
286
|
+
|
|
287
|
+
```tsx
|
|
288
|
+
import { useCallback } from "react";
|
|
289
|
+
import { motion, AnimatePresence } from "framer-motion";
|
|
290
|
+
import { useSound } from "@/hooks/use-sound";
|
|
291
|
+
import { uiSprite, SPRITE_MAP } from "@/sounds/ui-sprite";
|
|
292
|
+
import { hapticSuccess, haptic } from "@/utils/haptics";
|
|
293
|
+
import { CheckIcon } from "lucide-react";
|
|
294
|
+
|
|
295
|
+
interface TaskCompleteButtonProps {
|
|
296
|
+
completed: boolean;
|
|
297
|
+
onComplete: () => void;
|
|
298
|
+
soundEnabled?: boolean;
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
export function TaskCompleteButton({
|
|
302
|
+
completed,
|
|
303
|
+
onComplete,
|
|
304
|
+
soundEnabled = true,
|
|
305
|
+
}: TaskCompleteButtonProps) {
|
|
306
|
+
const { play } = useSound(uiSprite, {
|
|
307
|
+
volume: 0.4,
|
|
308
|
+
offset: SPRITE_MAP.success.offset,
|
|
309
|
+
duration: SPRITE_MAP.success.duration,
|
|
310
|
+
});
|
|
311
|
+
|
|
312
|
+
const prefersReducedMotion =
|
|
313
|
+
typeof window !== "undefined" &&
|
|
314
|
+
window.matchMedia("(prefers-reduced-motion: reduce)").matches;
|
|
315
|
+
|
|
316
|
+
const handleComplete = useCallback(() => {
|
|
317
|
+
if (completed) return;
|
|
318
|
+
|
|
319
|
+
// Fire all three channels simultaneously
|
|
320
|
+
onComplete();
|
|
321
|
+
if (soundEnabled) play();
|
|
322
|
+
hapticSuccess();
|
|
323
|
+
}, [completed, onComplete, soundEnabled, play]);
|
|
324
|
+
|
|
325
|
+
return (
|
|
326
|
+
<button
|
|
327
|
+
onClick={handleComplete}
|
|
328
|
+
className="relative flex items-center gap-2 rounded-lg bg-zinc-900
|
|
329
|
+
px-4 py-2 text-sm font-medium text-white
|
|
330
|
+
transition-colors hover:bg-zinc-800
|
|
331
|
+
disabled:opacity-50"
|
|
332
|
+
disabled={completed}
|
|
333
|
+
>
|
|
334
|
+
<AnimatePresence mode="wait">
|
|
335
|
+
{completed ? (
|
|
336
|
+
<motion.span
|
|
337
|
+
key="done"
|
|
338
|
+
initial={prefersReducedMotion ? {} : { scale: 0, opacity: 0 }}
|
|
339
|
+
animate={{ scale: 1, opacity: 1 }}
|
|
340
|
+
transition={{ type: "spring", stiffness: 500, damping: 25 }}
|
|
341
|
+
className="flex items-center gap-2 text-emerald-400"
|
|
342
|
+
>
|
|
343
|
+
<CheckIcon className="h-4 w-4" />
|
|
344
|
+
Done
|
|
345
|
+
</motion.span>
|
|
346
|
+
) : (
|
|
347
|
+
<motion.span
|
|
348
|
+
key="complete"
|
|
349
|
+
exit={prefersReducedMotion ? {} : { scale: 0.8, opacity: 0 }}
|
|
350
|
+
transition={{ duration: 0.1 }}
|
|
351
|
+
>
|
|
352
|
+
Mark Complete
|
|
353
|
+
</motion.span>
|
|
354
|
+
)}
|
|
355
|
+
</AnimatePresence>
|
|
356
|
+
</button>
|
|
357
|
+
);
|
|
358
|
+
}
|
|
359
|
+
```
|
|
360
|
+
|
|
361
|
+
### Timeline of a Multi-Sensory Moment
|
|
362
|
+
|
|
363
|
+
| Time | Visual | Sound | Haptic |
|
|
364
|
+
|------|--------|-------|--------|
|
|
365
|
+
| 0ms | Button state changes, checkmark scales in with spring animation | Success chime begins (100-150ms clip) | Double-pulse fires: 10ms on, 50ms gap, 10ms on |
|
|
366
|
+
| 150ms | Spring animation settles, color transition to emerald completes | Sound fades out naturally | Haptic complete |
|
|
367
|
+
| 300ms | Final resting state | Silent | Idle |
|
|
368
|
+
|
|
369
|
+
All three channels start at t=0 and resolve independently. The user perceives them as a single unified moment.
|
|
@@ -0,0 +1,176 @@
|
|
|
1
|
+
# Spatial Design Reference
|
|
2
|
+
|
|
3
|
+
## Table of Contents
|
|
4
|
+
1. Spacing Scale
|
|
5
|
+
2. Grid Systems
|
|
6
|
+
3. Visual Hierarchy
|
|
7
|
+
4. Whitespace
|
|
8
|
+
5. Layout Patterns
|
|
9
|
+
6. Common Mistakes
|
|
10
|
+
|
|
11
|
+
---
|
|
12
|
+
|
|
13
|
+
## 1. Spacing Scale
|
|
14
|
+
|
|
15
|
+
Use a consistent spacing scale based on a 4px unit. Never use arbitrary values like 13px or 7px.
|
|
16
|
+
|
|
17
|
+
```css
|
|
18
|
+
:root {
|
|
19
|
+
--space-1: 0.25rem; /* 4px - tight inline gaps */
|
|
20
|
+
--space-2: 0.5rem; /* 8px - icon-to-text, compact lists */
|
|
21
|
+
--space-3: 0.75rem; /* 12px - form field padding */
|
|
22
|
+
--space-4: 1rem; /* 16px - standard element spacing */
|
|
23
|
+
--space-5: 1.25rem; /* 20px - card padding */
|
|
24
|
+
--space-6: 1.5rem; /* 24px - section padding */
|
|
25
|
+
--space-8: 2rem; /* 32px - group separation */
|
|
26
|
+
--space-10: 2.5rem; /* 40px - major section gaps */
|
|
27
|
+
--space-12: 3rem; /* 48px - large section spacing */
|
|
28
|
+
--space-16: 4rem; /* 64px - page-level spacing */
|
|
29
|
+
--space-20: 5rem; /* 80px - hero-level breathing room */
|
|
30
|
+
--space-24: 6rem; /* 96px - dramatic separation */
|
|
31
|
+
}
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
### When to Use Which Size
|
|
35
|
+
- **1-2 (4-8px)**: Internal component spacing (icon + label, badge padding)
|
|
36
|
+
- **3-4 (12-16px)**: Component padding, list item spacing
|
|
37
|
+
- **5-6 (20-24px)**: Card padding, form group margins
|
|
38
|
+
- **8-10 (32-40px)**: Section separation within a page
|
|
39
|
+
- **12-16 (48-64px)**: Major content blocks, above/below fold
|
|
40
|
+
- **20-24 (80-96px)**: Hero areas, page-level breathing room
|
|
41
|
+
|
|
42
|
+
---
|
|
43
|
+
|
|
44
|
+
## 2. Grid Systems
|
|
45
|
+
|
|
46
|
+
### CSS Grid Defaults
|
|
47
|
+
```css
|
|
48
|
+
.grid {
|
|
49
|
+
display: grid;
|
|
50
|
+
grid-template-columns: repeat(auto-fill, minmax(280px, 1fr));
|
|
51
|
+
gap: var(--space-6);
|
|
52
|
+
}
|
|
53
|
+
```
|
|
54
|
+
|
|
55
|
+
### 12-Column Grid
|
|
56
|
+
For dashboard and editorial layouts:
|
|
57
|
+
```css
|
|
58
|
+
.page-grid {
|
|
59
|
+
display: grid;
|
|
60
|
+
grid-template-columns: repeat(12, 1fr);
|
|
61
|
+
gap: var(--space-6);
|
|
62
|
+
max-width: 1200px;
|
|
63
|
+
margin: 0 auto;
|
|
64
|
+
padding: 0 var(--space-6);
|
|
65
|
+
}
|
|
66
|
+
```
|
|
67
|
+
|
|
68
|
+
### Asymmetric Grids
|
|
69
|
+
For editorial and portfolio layouts, break the 12-column grid:
|
|
70
|
+
```css
|
|
71
|
+
.editorial-grid {
|
|
72
|
+
display: grid;
|
|
73
|
+
grid-template-columns: 2fr 1fr; /* 2:1 ratio */
|
|
74
|
+
gap: var(--space-8);
|
|
75
|
+
}
|
|
76
|
+
.portfolio-grid {
|
|
77
|
+
display: grid;
|
|
78
|
+
grid-template-columns: 3fr 2fr; /* golden-ish ratio */
|
|
79
|
+
gap: var(--space-6);
|
|
80
|
+
}
|
|
81
|
+
```
|
|
82
|
+
|
|
83
|
+
---
|
|
84
|
+
|
|
85
|
+
## 3. Visual Hierarchy
|
|
86
|
+
|
|
87
|
+
Hierarchy is established through size, weight, color, and space. Not all four at once. Pick two.
|
|
88
|
+
|
|
89
|
+
### Hierarchy Techniques (in order of strength)
|
|
90
|
+
1. **Size difference**: The fastest way to establish priority
|
|
91
|
+
2. **Weight difference**: Bold vs. regular within the same size
|
|
92
|
+
3. **Color contrast**: Primary vs. secondary text color
|
|
93
|
+
4. **Spatial separation**: More space around important elements
|
|
94
|
+
5. **Position**: Top-left gets seen first (in LTR languages)
|
|
95
|
+
|
|
96
|
+
### Rules
|
|
97
|
+
- If everything is bold, nothing is bold. Limit bold to headings and key data points.
|
|
98
|
+
- If everything is the same size, the eye has nowhere to land. Vary size by at least 1.5x between hierarchy levels.
|
|
99
|
+
- Use secondary/tertiary text colors for supporting information (timestamps, metadata, helper text).
|
|
100
|
+
- Reduce visual weight of labels and increase weight of values in data-heavy UIs.
|
|
101
|
+
|
|
102
|
+
---
|
|
103
|
+
|
|
104
|
+
## 4. Whitespace
|
|
105
|
+
|
|
106
|
+
Whitespace is not wasted space. It is a design element.
|
|
107
|
+
|
|
108
|
+
### Internal vs. External Spacing
|
|
109
|
+
- **Internal**: Padding inside a component (card padding, button padding). Relates to the component's own content.
|
|
110
|
+
- **External**: Margin between components. Relates to the component's relationship with siblings.
|
|
111
|
+
|
|
112
|
+
### Gestalt Grouping
|
|
113
|
+
Elements that are closer together are perceived as related. Use tighter spacing within groups and wider spacing between groups. The ratio between intra-group and inter-group spacing should be at least 2:1 (e.g., 8px within, 24px between).
|
|
114
|
+
|
|
115
|
+
### Generous vs. Dense
|
|
116
|
+
- **Generous**: Marketing pages, portfolios, editorial content. Use 80-120px between major sections.
|
|
117
|
+
- **Dense**: Dashboards, admin panels, data tables. Use 16-32px between sections but maintain consistent rhythm.
|
|
118
|
+
|
|
119
|
+
---
|
|
120
|
+
|
|
121
|
+
## 5. Layout Patterns
|
|
122
|
+
|
|
123
|
+
### Centered Content
|
|
124
|
+
Max-width container with auto margins. Standard max-widths: 640px (narrow/reading), 960px (medium), 1200px (wide), 1440px (ultrawide).
|
|
125
|
+
|
|
126
|
+
### Sidebar + Main
|
|
127
|
+
```css
|
|
128
|
+
.layout {
|
|
129
|
+
display: grid;
|
|
130
|
+
grid-template-columns: 260px 1fr;
|
|
131
|
+
min-height: 100vh;
|
|
132
|
+
}
|
|
133
|
+
```
|
|
134
|
+
|
|
135
|
+
### Sticky Header + Scrollable Content
|
|
136
|
+
```css
|
|
137
|
+
.header { position: sticky; top: 0; z-index: 10; }
|
|
138
|
+
.main { overflow-y: auto; }
|
|
139
|
+
```
|
|
140
|
+
|
|
141
|
+
### Bento Grid
|
|
142
|
+
Irregular grid with varied cell sizes:
|
|
143
|
+
```css
|
|
144
|
+
.bento {
|
|
145
|
+
display: grid;
|
|
146
|
+
grid-template-columns: repeat(4, 1fr);
|
|
147
|
+
grid-auto-rows: 200px;
|
|
148
|
+
gap: var(--space-4);
|
|
149
|
+
}
|
|
150
|
+
.bento .featured {
|
|
151
|
+
grid-column: span 2;
|
|
152
|
+
grid-row: span 2;
|
|
153
|
+
}
|
|
154
|
+
```
|
|
155
|
+
|
|
156
|
+
### Overlap/Layer
|
|
157
|
+
Elements that break out of the grid create visual tension:
|
|
158
|
+
```css
|
|
159
|
+
.overlap-element {
|
|
160
|
+
position: relative;
|
|
161
|
+
z-index: 2;
|
|
162
|
+
margin-top: -3rem; /* pulls up into previous section */
|
|
163
|
+
}
|
|
164
|
+
```
|
|
165
|
+
|
|
166
|
+
---
|
|
167
|
+
|
|
168
|
+
## 6. Common Mistakes
|
|
169
|
+
|
|
170
|
+
- Using inconsistent spacing values (17px here, 23px there)
|
|
171
|
+
- Centering everything on the page (creates a vertical highway with no anchor points)
|
|
172
|
+
- Not using max-width on content (text that spans 1400px is unreadable)
|
|
173
|
+
- Applying the same padding to all components regardless of their content
|
|
174
|
+
- Putting too many items in a row on mobile (three columns at 375px is almost never right)
|
|
175
|
+
- Using margin-top and margin-bottom on the same element (pick one direction, usually bottom, and stick with it throughout the project)
|
|
176
|
+
- Neglecting the space between the last element and the container edge
|