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.
- package/README.md +67 -65
- package/agents/picasso.md +36 -15
- package/bin/install.mjs +1 -1
- package/package.json +2 -2
- package/references/animation-performance.md +244 -0
- package/references/brand-and-identity.md +136 -0
- package/references/code-typography.md +222 -0
- package/references/dark-mode.md +199 -0
- package/references/i18n-visual-patterns.md +177 -0
- package/references/images-and-media.md +222 -0
- package/references/loading-and-states.md +258 -0
- package/references/micro-interactions.md +291 -0
- package/references/navigation-patterns.md +247 -0
- package/references/tables-and-forms.md +227 -0
- package/skills/picasso/SKILL.md +10 -1
- package/templates/picasso-config.md +53 -0
- package/checklists/pre-ship.md +0 -83
|
@@ -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.
|