picasso-skill 2.0.0 → 2.0.2

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.
@@ -0,0 +1,177 @@
1
+ # Internationalization Visual Patterns Reference
2
+
3
+ ## Table of Contents
4
+ 1. Logical Properties
5
+ 2. RTL Layout Mirroring
6
+ 3. Text Expansion by Language
7
+ 4. CJK Text Rendering
8
+ 5. Number and Currency Formatting
9
+ 6. Font Stacks for Multi-Language
10
+ 7. Icon Mirroring in RTL
11
+ 8. Common Mistakes
12
+
13
+ ---
14
+
15
+ ## 1. Logical Properties
16
+
17
+ Replace physical properties with logical ones. This makes RTL support automatic.
18
+
19
+ | Physical (avoid) | Logical (use) |
20
+ |---|---|
21
+ | `margin-left` | `margin-inline-start` |
22
+ | `margin-right` | `margin-inline-end` |
23
+ | `padding-left` | `padding-inline-start` |
24
+ | `text-align: left` | `text-align: start` |
25
+ | `float: left` | `float: inline-start` |
26
+ | `border-left` | `border-inline-start` |
27
+ | `left: 0` | `inset-inline-start: 0` |
28
+
29
+ ```css
30
+ /* Good: works in both LTR and RTL */
31
+ .sidebar { margin-inline-end: 2rem; padding-inline-start: 1rem; }
32
+
33
+ /* Bad: breaks in RTL */
34
+ .sidebar { margin-right: 2rem; padding-left: 1rem; }
35
+ ```
36
+
37
+ ---
38
+
39
+ ## 2. RTL Layout Mirroring
40
+
41
+ Set `dir="auto"` on user-generated content. Set `dir="rtl"` on the `<html>` element for RTL languages.
42
+
43
+ ```html
44
+ <html lang="ar" dir="rtl">
45
+ ```
46
+
47
+ Flexbox and Grid automatically reverse in RTL when using logical properties. No extra CSS needed.
48
+
49
+ ```css
50
+ /* This works in both directions automatically */
51
+ .nav { display: flex; gap: 1rem; }
52
+ .card { display: grid; grid-template-columns: auto 1fr; }
53
+ ```
54
+
55
+ ---
56
+
57
+ ## 3. Text Expansion by Language
58
+
59
+ English text expands significantly when translated. Design for the longest likely translation.
60
+
61
+ | Language | Expansion from English |
62
+ |---|---|
63
+ | German | +30-35% |
64
+ | French | +15-20% |
65
+ | Finnish | +30-40% |
66
+ | Russian | +15-25% |
67
+ | Chinese | -30-50% (shorter) |
68
+ | Japanese | -20-40% (shorter) |
69
+ | Arabic | +20-25% |
70
+
71
+ Rules:
72
+ - Never use fixed-width containers for translatable text.
73
+ - Buttons: use `min-width` not `width`. Allow text to wrap or grow.
74
+ - Navigation: test with German translations (longest common language).
75
+ - Truncate with `text-overflow: ellipsis` as a last resort, never as the design.
76
+
77
+ ```css
78
+ /* Good: grows with content */
79
+ .btn { min-width: 120px; padding-inline: 1.5rem; white-space: nowrap; }
80
+
81
+ /* Bad: text overflows in German */
82
+ .btn { width: 120px; }
83
+ ```
84
+
85
+ ---
86
+
87
+ ## 4. CJK Text Rendering
88
+
89
+ Chinese, Japanese, and Korean text has different line-breaking and spacing rules.
90
+
91
+ ```css
92
+ /* Allow CJK text to break at any character */
93
+ .cjk-text {
94
+ line-break: auto;
95
+ word-break: keep-all; /* Korean: don't break within words */
96
+ overflow-wrap: break-word;
97
+ }
98
+
99
+ /* CJK doesn't need letter-spacing for readability */
100
+ :lang(zh), :lang(ja), :lang(ko) {
101
+ letter-spacing: 0;
102
+ }
103
+ ```
104
+
105
+ CJK text is denser — reduce line-height slightly:
106
+ ```css
107
+ :lang(zh), :lang(ja) { line-height: 1.7; } /* vs 1.5 for Latin */
108
+ ```
109
+
110
+ ---
111
+
112
+ ## 5. Number and Currency Formatting
113
+
114
+ Never hardcode currency symbols or number formats. Use `Intl.NumberFormat`.
115
+
116
+ ```js
117
+ // Automatic locale-aware formatting
118
+ new Intl.NumberFormat('en-US', { style: 'currency', currency: 'USD' }).format(1234.56)
119
+ // → "$1,234.56"
120
+
121
+ new Intl.NumberFormat('de-DE', { style: 'currency', currency: 'EUR' }).format(1234.56)
122
+ // → "1.234,56 €"
123
+
124
+ new Intl.NumberFormat('ja-JP', { style: 'currency', currency: 'JPY' }).format(1234)
125
+ // → "¥1,234"
126
+ ```
127
+
128
+ For dates: use `Intl.DateTimeFormat`, never manual formatting.
129
+
130
+ ---
131
+
132
+ ## 6. Font Stacks for Multi-Language
133
+
134
+ Include system fonts per script as fallbacks:
135
+
136
+ ```css
137
+ body {
138
+ font-family:
139
+ 'Your Custom Font', /* Latin */
140
+ 'Noto Sans SC', /* Simplified Chinese */
141
+ 'Noto Sans JP', /* Japanese */
142
+ 'Noto Sans KR', /* Korean */
143
+ 'Noto Sans Arabic', /* Arabic */
144
+ system-ui, sans-serif; /* Fallback */
145
+ }
146
+ ```
147
+
148
+ Google's Noto family covers every Unicode script. Use it as the universal fallback.
149
+
150
+ ---
151
+
152
+ ## 7. Icon Mirroring in RTL
153
+
154
+ Icons that imply direction MUST mirror in RTL. Icons that don't imply direction must NOT.
155
+
156
+ **Mirror in RTL:** arrows, back/forward, reply, undo/redo, text indent, send, search (if it implies reading direction), progress bars.
157
+
158
+ **Do NOT mirror:** play/pause, checkmarks, plus/minus, clock, globe, user, settings gear, download, external link.
159
+
160
+ ```css
161
+ [dir="rtl"] .icon-directional {
162
+ transform: scaleX(-1);
163
+ }
164
+ ```
165
+
166
+ ---
167
+
168
+ ## 8. Common Mistakes
169
+
170
+ - **Hardcoding `left`/`right` in CSS.** Use logical properties.
171
+ - **Fixed-width buttons.** They overflow in German/Finnish. Use `min-width`.
172
+ - **Testing only in English.** Design breaks with 35% longer strings.
173
+ - **`text-align: left` instead of `text-align: start`.** Breaks RTL.
174
+ - **Mirroring ALL icons in RTL.** Only directional icons should flip.
175
+ - **Hardcoding date formats.** `01/02/2026` means different dates in US vs UK. Use `Intl.DateTimeFormat`.
176
+ - **Not loading CJK fonts.** System fonts for CJK vary wildly. Include Noto Sans.
177
+ - **Ignoring `dir="auto"` on user content.** A Hebrew comment in an English page needs auto-detection.
@@ -0,0 +1,222 @@
1
+ # Images and Media Reference
2
+
3
+ ## Table of Contents
4
+ 1. Format Selection
5
+ 2. Responsive Images
6
+ 3. Preventing Layout Shift
7
+ 4. Image as Design Element
8
+ 5. Avatar Systems
9
+ 6. Favicon and App Icons
10
+ 7. Open Graph Images
11
+ 8. Video Backgrounds
12
+ 9. Common Mistakes
13
+
14
+ ---
15
+
16
+ ## 1. Format Selection
17
+
18
+ | Format | Use Case | Browser Support |
19
+ |---|---|---|
20
+ | AVIF | Best compression for photos, modern browsers | Chrome, Firefox, Safari 16.4+ |
21
+ | WebP | General purpose, wide support | All modern browsers |
22
+ | JPEG | Photos, fallback for older browsers | Universal |
23
+ | PNG | Transparency, screenshots, UI elements | Universal |
24
+ | SVG | Icons, illustrations, logos — scales infinitely | Universal |
25
+
26
+ Decision tree:
27
+ - Is it an icon or illustration? → **SVG**
28
+ - Does it need transparency? → **PNG** (or WebP with alpha)
29
+ - Is it a photo? → **AVIF** with WebP fallback, JPEG last resort
30
+
31
+ ```html
32
+ <picture>
33
+ <source srcset="hero.avif" type="image/avif" />
34
+ <source srcset="hero.webp" type="image/webp" />
35
+ <img src="hero.jpg" alt="Hero description" width="1200" height="630" loading="lazy" />
36
+ </picture>
37
+ ```
38
+
39
+ ---
40
+
41
+ ## 2. Responsive Images
42
+
43
+ Always provide multiple sizes. The browser picks the best one.
44
+
45
+ ```html
46
+ <img
47
+ src="product-800.webp"
48
+ srcset="product-400.webp 400w, product-800.webp 800w, product-1200.webp 1200w"
49
+ sizes="(max-width: 768px) 100vw, (max-width: 1200px) 50vw, 600px"
50
+ alt="Product shot"
51
+ width="800"
52
+ height="600"
53
+ loading="lazy"
54
+ decoding="async"
55
+ />
56
+ ```
57
+
58
+ Rules:
59
+ - `sizes` must match your CSS layout. If the image is 50% of viewport on desktop, say `50vw`.
60
+ - Generate 3-4 variants: 400w, 800w, 1200w, 2400w (for retina).
61
+ - Use `loading="lazy"` on everything EXCEPT the LCP image (above the fold).
62
+ - Use `decoding="async"` on all images.
63
+
64
+ ---
65
+
66
+ ## 3. Preventing Layout Shift
67
+
68
+ Every `<img>` without dimensions causes CLS. Always set width and height OR use aspect-ratio.
69
+
70
+ ```html
71
+ <!-- Option 1: Explicit dimensions -->
72
+ <img src="photo.webp" width="800" height="600" alt="..." />
73
+
74
+ <!-- Option 2: CSS aspect-ratio -->
75
+ <img src="photo.webp" class="w-full aspect-[4/3] object-cover" alt="..." />
76
+ ```
77
+
78
+ For Next.js Image component, width/height are required. Use `fill` prop with a sized container for responsive images.
79
+
80
+ ---
81
+
82
+ ## 4. Image as Design Element
83
+
84
+ ```css
85
+ /* Cover: fill container, crop to fit */
86
+ .hero-img { object-fit: cover; object-position: center 30%; }
87
+
88
+ /* Contain: show full image, letterbox if needed */
89
+ .product-img { object-fit: contain; }
90
+
91
+ /* Gradient overlay on image */
92
+ .card-img-overlay {
93
+ position: relative;
94
+ }
95
+ .card-img-overlay::after {
96
+ content: '';
97
+ position: absolute;
98
+ inset: 0;
99
+ background: linear-gradient(to top, oklch(0 0 0 / 0.7), transparent 60%);
100
+ }
101
+ ```
102
+
103
+ ---
104
+
105
+ ## 5. Avatar Systems
106
+
107
+ Consistent sizing scale for avatars:
108
+
109
+ ```css
110
+ .avatar-xs { width: 24px; height: 24px; } /* inline mentions */
111
+ .avatar-sm { width: 32px; height: 32px; } /* list items */
112
+ .avatar-md { width: 40px; height: 40px; } /* cards, comments */
113
+ .avatar-lg { width: 56px; height: 56px; } /* profiles */
114
+ .avatar-xl { width: 80px; height: 80px; } /* hero profiles */
115
+ ```
116
+
117
+ Fallback for missing avatars: use initials on a deterministic background color.
118
+
119
+ ```jsx
120
+ function Avatar({ name, src, size = 'md' }) {
121
+ const initials = name.split(' ').map(n => n[0]).join('').slice(0, 2);
122
+ const hue = name.split('').reduce((acc, c) => acc + c.charCodeAt(0), 0) % 360;
123
+
124
+ if (src) return <img src={src} alt={name} className={`avatar-${size} rounded-full`} />;
125
+ return (
126
+ <div className={`avatar-${size} rounded-full flex items-center justify-center font-bold text-white`}
127
+ style={{ background: `oklch(0.55 0.15 ${hue})` }}>
128
+ {initials}
129
+ </div>
130
+ );
131
+ }
132
+ ```
133
+
134
+ ---
135
+
136
+ ## 6. Favicon and App Icons
137
+
138
+ Generate all required formats:
139
+
140
+ | File | Size | Use |
141
+ |---|---|---|
142
+ | `favicon.ico` | 32x32 | Legacy browsers |
143
+ | `favicon.svg` | Scalable | Modern browsers (supports dark mode via CSS) |
144
+ | `apple-touch-icon.png` | 180x180 | iOS home screen |
145
+ | `icon-192.png` | 192x192 | Android/PWA |
146
+ | `icon-512.png` | 512x512 | PWA splash screen |
147
+
148
+ ```html
149
+ <link rel="icon" href="/favicon.svg" type="image/svg+xml" />
150
+ <link rel="icon" href="/favicon.ico" sizes="32x32" />
151
+ <link rel="apple-touch-icon" href="/apple-touch-icon.png" />
152
+ ```
153
+
154
+ SVG favicon supports dark mode:
155
+ ```svg
156
+ <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 32 32">
157
+ <style>
158
+ circle { fill: #1a1a2e; }
159
+ @media (prefers-color-scheme: dark) { circle { fill: #e0e0ff; } }
160
+ </style>
161
+ <circle cx="16" cy="16" r="14" />
162
+ </svg>
163
+ ```
164
+
165
+ ---
166
+
167
+ ## 7. Open Graph Images
168
+
169
+ OG images control how your site appears in social shares. Size: **1200x630px**.
170
+
171
+ Rules:
172
+ - Text must be legible at thumbnail size (~300px wide)
173
+ - Use 48-72px font size minimum for titles
174
+ - High contrast text on background
175
+ - Include brand logo (small, corner)
176
+ - Don't rely on small text — it's unreadable in feeds
177
+
178
+ ```html
179
+ <meta property="og:image" content="https://example.com/og-image.png" />
180
+ <meta property="og:image:width" content="1200" />
181
+ <meta property="og:image:height" content="630" />
182
+ <meta property="og:image:alt" content="Product name — tagline" />
183
+ ```
184
+
185
+ ---
186
+
187
+ ## 8. Video Backgrounds
188
+
189
+ Use sparingly. Auto-playing background video must be:
190
+ - Muted (`muted` attribute required for autoplay)
191
+ - Short (5-15 seconds, looped)
192
+ - Compressed aggressively (< 2MB)
193
+ - Has a poster image fallback
194
+ - Respects `prefers-reduced-motion` (show poster only)
195
+
196
+ ```html
197
+ <video autoplay muted loop playsinline poster="fallback.webp"
198
+ class="absolute inset-0 w-full h-full object-cover">
199
+ <source src="bg.mp4" type="video/mp4" />
200
+ </video>
201
+ ```
202
+
203
+ ```css
204
+ @media (prefers-reduced-motion: reduce) {
205
+ video[autoplay] { display: none; }
206
+ /* Poster image shows via the parent's background */
207
+ }
208
+ ```
209
+
210
+ ---
211
+
212
+ ## 9. Common Mistakes
213
+
214
+ - **No `width`/`height` on images.** Causes layout shift. Always set dimensions.
215
+ - **`loading="lazy"` on the hero image.** The LCP image must load eagerly.
216
+ - **JPEG for everything.** Use AVIF/WebP for 50-80% size reduction.
217
+ - **Images without `alt` text.** Decorative images use `alt=""`, meaningful images describe the content.
218
+ - **Retina images at 1x.** Serve 2x resolution for high-DPI screens via srcset.
219
+ - **No `decoding="async"`.** Blocks main thread without it.
220
+ - **Favicon only as .ico.** Modern browsers prefer SVG (scales, supports dark mode).
221
+ - **OG images with small text.** Unreadable in social feeds. Minimum 48px font.
222
+ - **Background video without `prefers-reduced-motion` check.** Accessibility violation.
@@ -0,0 +1,258 @@
1
+ # Loading and States Reference
2
+
3
+ ## Table of Contents
4
+ 1. Loading Duration Rules
5
+ 2. Skeleton Screens
6
+ 3. Spinner Use Cases
7
+ 4. Optimistic Updates
8
+ 5. Error Boundaries
9
+ 6. Retry Patterns
10
+ 7. Offline and Degraded States
11
+ 8. Empty States
12
+ 9. Common Mistakes
13
+
14
+ ---
15
+
16
+ ## 1. Loading Duration Rules
17
+
18
+ | Duration | UI Response |
19
+ |---|---|
20
+ | < 100ms | No indicator. Feels instant. |
21
+ | 100ms - 1s | Subtle opacity fade or pulse on the trigger element. |
22
+ | 1s - 3s | Skeleton screen replacing the content area. |
23
+ | 3s - 10s | Progress bar (determinate if possible, indeterminate if not). |
24
+ | 10s+ | Progress bar + estimated time remaining + ability to cancel. |
25
+
26
+ Never show a spinner for content that takes > 1s. Skeleton screens preserve spatial layout and feel faster.
27
+
28
+ ---
29
+
30
+ ## 2. Skeleton Screens
31
+
32
+ Replace content with gray placeholder blocks that match the layout. Add a shimmer animation.
33
+
34
+ ```css
35
+ .skeleton {
36
+ background: var(--surface-2);
37
+ border-radius: 4px;
38
+ position: relative;
39
+ overflow: hidden;
40
+ }
41
+
42
+ .skeleton::after {
43
+ content: '';
44
+ position: absolute;
45
+ inset: 0;
46
+ background: linear-gradient(
47
+ 90deg,
48
+ transparent 0%,
49
+ oklch(1 0 0 / 0.04) 50%,
50
+ transparent 100%
51
+ );
52
+ background-size: 200% 100%;
53
+ animation: shimmer 1.5s ease-in-out infinite;
54
+ }
55
+
56
+ @keyframes shimmer {
57
+ 0% { background-position: 200% 0; }
58
+ 100% { background-position: -200% 0; }
59
+ }
60
+ ```
61
+
62
+ ```jsx
63
+ function CardSkeleton() {
64
+ return (
65
+ <div className="card p-4 space-y-3">
66
+ <div className="skeleton h-4 w-3/4 rounded" />
67
+ <div className="skeleton h-3 w-full rounded" />
68
+ <div className="skeleton h-3 w-5/6 rounded" />
69
+ <div className="skeleton h-8 w-24 rounded-lg mt-4" />
70
+ </div>
71
+ );
72
+ }
73
+ ```
74
+
75
+ Progressive skeletons: show real content as it loads, replacing skeletons one section at a time (top to bottom).
76
+
77
+ ---
78
+
79
+ ## 3. Spinner Use Cases
80
+
81
+ Spinners are ONLY for small, inline actions:
82
+ - Button submit (replace button text with spinner)
83
+ - Toggle switch (brief loading)
84
+ - Inline save indicators
85
+
86
+ Never for:
87
+ - Page content areas (use skeleton)
88
+ - Full-page loading (use progress bar or skeleton)
89
+ - Lists or grids (use skeleton)
90
+
91
+ ```css
92
+ .spinner {
93
+ width: 16px;
94
+ height: 16px;
95
+ border: 2px solid var(--border);
96
+ border-top-color: var(--accent);
97
+ border-radius: 50%;
98
+ animation: spin 0.6s linear infinite;
99
+ }
100
+
101
+ @keyframes spin { to { transform: rotate(360deg); } }
102
+ ```
103
+
104
+ ---
105
+
106
+ ## 4. Optimistic Updates
107
+
108
+ Update the UI immediately before the server confirms. Roll back if the server rejects.
109
+
110
+ ```jsx
111
+ async function toggleFavorite(id) {
112
+ // Optimistically update UI
113
+ setItems(prev => prev.map(item =>
114
+ item.id === id ? { ...item, favorited: !item.favorited } : item
115
+ ));
116
+
117
+ try {
118
+ await api.toggleFavorite(id);
119
+ } catch {
120
+ // Revert on failure
121
+ setItems(prev => prev.map(item =>
122
+ item.id === id ? { ...item, favorited: !item.favorited } : item
123
+ ));
124
+ toast.error('Failed to update. Please try again.');
125
+ }
126
+ }
127
+ ```
128
+
129
+ Use for: likes, favorites, toggles, reordering, marking as read. Don't use for: payments, deletions, irreversible actions.
130
+
131
+ ---
132
+
133
+ ## 5. Error Boundaries
134
+
135
+ React error boundaries catch rendering errors and show a fallback UI.
136
+
137
+ ```jsx
138
+ // app/error.tsx (Next.js App Router)
139
+ 'use client';
140
+
141
+ export default function Error({ error, reset }) {
142
+ return (
143
+ <div className="flex flex-col items-center justify-center min-h-[400px] gap-4">
144
+ <div className="h-12 w-12 rounded-xl bg-red/10 flex items-center justify-center">
145
+ <svg className="w-6 h-6 text-red">...</svg>
146
+ </div>
147
+ <h2 className="text-lg font-semibold">Something went wrong</h2>
148
+ <p className="text-sm text-secondary max-w-md text-center">{error.message}</p>
149
+ <button onClick={reset} className="btn-primary">Try again</button>
150
+ </div>
151
+ );
152
+ }
153
+ ```
154
+
155
+ Place error boundaries at layout boundaries (per page section, not per component).
156
+
157
+ ---
158
+
159
+ ## 6. Retry Patterns
160
+
161
+ Exponential backoff: 1s → 2s → 4s. Max 3 retries. Show retry count to user.
162
+
163
+ ```jsx
164
+ async function fetchWithRetry(url, maxRetries = 3) {
165
+ for (let i = 0; i < maxRetries; i++) {
166
+ try {
167
+ const res = await fetch(url);
168
+ if (!res.ok) throw new Error(`HTTP ${res.status}`);
169
+ return res.json();
170
+ } catch (err) {
171
+ if (i === maxRetries - 1) throw err;
172
+ await new Promise(r => setTimeout(r, Math.pow(2, i) * 1000));
173
+ }
174
+ }
175
+ }
176
+ ```
177
+
178
+ UI: after max retries, show error state with manual retry button. Don't auto-retry forever.
179
+
180
+ ---
181
+
182
+ ## 7. Offline and Degraded States
183
+
184
+ Detect online/offline status:
185
+
186
+ ```js
187
+ const [isOnline, setIsOnline] = useState(navigator.onLine);
188
+ useEffect(() => {
189
+ const on = () => setIsOnline(true);
190
+ const off = () => setIsOnline(false);
191
+ window.addEventListener('online', on);
192
+ window.addEventListener('offline', off);
193
+ return () => { window.removeEventListener('online', on); window.removeEventListener('offline', off); };
194
+ }, []);
195
+ ```
196
+
197
+ Offline banner: fixed at top, yellow/amber, with "You're offline. Changes will sync when reconnected."
198
+
199
+ Stale-while-revalidate: show cached data immediately, fetch fresh in background, update silently.
200
+
201
+ ---
202
+
203
+ ## 8. Empty States
204
+
205
+ Five types, each with different copy and actions:
206
+
207
+ | Type | Heading | Action |
208
+ |---|---|---|
209
+ | **First use** | "No projects yet" | "Create your first project" (primary CTA) |
210
+ | **No results** | "No results for 'xyz'" | "Try a different search" or clear filters |
211
+ | **Deleted** | "This item was deleted" | "Undo" (within 10s) or go back |
212
+ | **Error** | "Couldn't load data" | "Retry" button |
213
+ | **Permission** | "You don't have access" | "Request access" or contact admin |
214
+
215
+ ```jsx
216
+ function EmptyState({ type, query }) {
217
+ const config = {
218
+ 'first-use': {
219
+ icon: <PlusIcon />,
220
+ title: 'No wallets yet',
221
+ description: 'Add your first wallet to start tracking transactions.',
222
+ action: { label: 'Add Wallet', onClick: openAddModal },
223
+ },
224
+ 'no-results': {
225
+ icon: <SearchIcon />,
226
+ title: `No results for "${query}"`,
227
+ description: 'Try adjusting your search or filters.',
228
+ action: { label: 'Clear filters', onClick: clearFilters },
229
+ },
230
+ };
231
+
232
+ const c = config[type];
233
+ return (
234
+ <div className="flex flex-col items-center py-16 gap-3">
235
+ <div className="h-12 w-12 rounded-xl bg-surface-2 flex items-center justify-center text-muted">
236
+ {c.icon}
237
+ </div>
238
+ <h3 className="text-sm font-semibold">{c.title}</h3>
239
+ <p className="text-xs text-muted max-w-sm text-center">{c.description}</p>
240
+ {c.action && <button className="btn-primary mt-2" onClick={c.action.onClick}>{c.action.label}</button>}
241
+ </div>
242
+ );
243
+ }
244
+ ```
245
+
246
+ ---
247
+
248
+ ## 9. Common Mistakes
249
+
250
+ - **Spinner for content areas.** Always skeleton. Spinners for inline actions only.
251
+ - **No loading indicator for 1-3s waits.** Users think the app is broken. Show skeleton.
252
+ - **Optimistic updates for irreversible actions.** Only for safe, reversible changes.
253
+ - **Error boundary catching everything silently.** Show the error. Let users retry.
254
+ - **Infinite auto-retry.** Max 3 retries, then show manual retry button.
255
+ - **Generic empty states.** "No data" is useless. Be specific about what to do next.
256
+ - **Same empty state for first-use and no-results.** They have different user intent.
257
+ - **No offline detection.** Users submit forms offline and lose data. Detect and warn.
258
+ - **Loading skeleton without shimmer.** Static gray blocks look broken. Add the shimmer.