@musashishao/agent-kit 1.7.0 → 1.8.1
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/.agent/rules/CODEX.md +8 -7
- package/.agent/rules/CODE_RULES.md +15 -0
- package/.agent/rules/REFERENCE.md +7 -2
- package/.agent/skills/brainstorming/SKILL.md +52 -0
- package/.agent/skills/frontend-design/SKILL.md +30 -0
- package/.agent/skills/git-worktrees/SKILL.md +181 -0
- package/.agent/skills/parallel-agents/SKILL.md +89 -0
- package/.agent/skills/react-patterns/SKILL.md +31 -0
- package/.agent/skills/react-patterns/bundle-patterns.md +129 -0
- package/.agent/skills/react-patterns/rerender-patterns.md +163 -0
- package/.agent/skills/react-patterns/server-patterns.md +146 -0
- package/.agent/skills/react-patterns/waterfall-patterns.md +102 -0
- package/.agent/skills/reader-testing/SKILL.md +183 -0
- package/.agent/skills/verification-gate/SKILL.md +129 -0
- package/.agent/skills/web-interface-guidelines/SKILL.md +94 -0
- package/.agent/skills/web-interface-guidelines/animation.md +153 -0
- package/.agent/skills/web-interface-guidelines/anti-patterns.md +204 -0
- package/.agent/skills/web-interface-guidelines/focus-states.md +104 -0
- package/.agent/skills/web-interface-guidelines/forms.md +154 -0
- package/.agent/skills/web-interface-guidelines/touch-interaction.md +150 -0
- package/.agent/workflows/autofix.md +1 -1
- package/.agent/workflows/brainstorm.md +1 -1
- package/.agent/workflows/context.md +1 -1
- package/.agent/workflows/create.md +1 -1
- package/.agent/workflows/dashboard.md +1 -1
- package/.agent/workflows/debug.md +1 -1
- package/.agent/workflows/deploy.md +1 -1
- package/.agent/workflows/enhance.md +1 -1
- package/.agent/workflows/next.md +1 -1
- package/.agent/workflows/orchestrate.md +1 -1
- package/.agent/workflows/plan.md +1 -1
- package/.agent/workflows/preview.md +1 -1
- package/.agent/workflows/quality.md +1 -1
- package/.agent/workflows/spec.md +1 -1
- package/.agent/workflows/status.md +1 -1
- package/.agent/workflows/test.md +1 -1
- package/.agent/workflows/ui-ux-pro-max.md +1 -1
- package/README.md +3 -3
- package/package.json +1 -1
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: web-interface-guidelines
|
|
3
|
+
description: Web Interface Guidelines audit for accessibility, forms, touch, animation, and anti-patterns. Use when reviewing UI code, checking accessibility, or auditing design. Based on Vercel's 100+ rules.
|
|
4
|
+
allowed-tools: Read, Write, Edit, Glob, Grep, Bash
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
# Web Interface Guidelines
|
|
8
|
+
|
|
9
|
+
> Audit UI code for compliance with modern web interface best practices.
|
|
10
|
+
> Based on [Vercel Web Interface Guidelines](https://github.com/vercel-labs/web-interface-guidelines).
|
|
11
|
+
|
|
12
|
+
---
|
|
13
|
+
|
|
14
|
+
## 🎯 When to Apply
|
|
15
|
+
|
|
16
|
+
| Trigger Phrase | Action |
|
|
17
|
+
|----------------|--------|
|
|
18
|
+
| "Review my UI" | Full audit |
|
|
19
|
+
| "Check accessibility" | Focus, forms, aria |
|
|
20
|
+
| "Audit design" | All categories |
|
|
21
|
+
| "Check my site" | Anti-patterns scan |
|
|
22
|
+
|
|
23
|
+
---
|
|
24
|
+
|
|
25
|
+
## 📚 Modular Files
|
|
26
|
+
|
|
27
|
+
Read ONLY the sections relevant to your task:
|
|
28
|
+
|
|
29
|
+
| File | Categories |
|
|
30
|
+
|------|-----------|
|
|
31
|
+
| [forms.md](forms.md) | Input, autocomplete, validation |
|
|
32
|
+
| [focus-states.md](focus-states.md) | Focus, keyboard navigation |
|
|
33
|
+
| [touch-interaction.md](touch-interaction.md) | Mobile, touch, safe areas |
|
|
34
|
+
| [animation.md](animation.md) | Motion, transitions |
|
|
35
|
+
| [anti-patterns.md](anti-patterns.md) | ❌ What NOT to do |
|
|
36
|
+
|
|
37
|
+
---
|
|
38
|
+
|
|
39
|
+
## Quick Reference
|
|
40
|
+
|
|
41
|
+
### Accessibility (A11y)
|
|
42
|
+
|
|
43
|
+
| Rule | Check |
|
|
44
|
+
|------|-------|
|
|
45
|
+
| Visible focus | `focus-visible:ring-*` present |
|
|
46
|
+
| No outline:none | Unless replacement exists |
|
|
47
|
+
| Button for actions | Not `<div onClick>` |
|
|
48
|
+
| Labels on inputs | `<label>` or `aria-label` |
|
|
49
|
+
| Icon buttons | Has `aria-label` |
|
|
50
|
+
|
|
51
|
+
### Forms
|
|
52
|
+
|
|
53
|
+
| Rule | Check |
|
|
54
|
+
|------|-------|
|
|
55
|
+
| autocomplete | Present on inputs |
|
|
56
|
+
| Correct type | `email`, `tel`, `url` |
|
|
57
|
+
| No paste block | Never `onPaste preventDefault` |
|
|
58
|
+
| Error placement | Inline next to field |
|
|
59
|
+
|
|
60
|
+
### Animation
|
|
61
|
+
|
|
62
|
+
| Rule | Check |
|
|
63
|
+
|------|-------|
|
|
64
|
+
| Reduced motion | `prefers-reduced-motion` honored |
|
|
65
|
+
| Compositor only | `transform`, `opacity` only |
|
|
66
|
+
| No `transition: all` | List properties explicitly |
|
|
67
|
+
|
|
68
|
+
### Touch & Mobile
|
|
69
|
+
|
|
70
|
+
| Rule | Check |
|
|
71
|
+
|------|-------|
|
|
72
|
+
| Touch action | `touch-action: manipulation` |
|
|
73
|
+
| Safe areas | `env(safe-area-inset-*)` |
|
|
74
|
+
| Scroll contain | `overscroll-behavior: contain` in modals |
|
|
75
|
+
|
|
76
|
+
---
|
|
77
|
+
|
|
78
|
+
## Output Format
|
|
79
|
+
|
|
80
|
+
When auditing, output findings in this format:
|
|
81
|
+
|
|
82
|
+
```
|
|
83
|
+
filename:line - [CATEGORY] Description of issue
|
|
84
|
+
```
|
|
85
|
+
|
|
86
|
+
Example:
|
|
87
|
+
```
|
|
88
|
+
components/Button.tsx:15 - [FOCUS] Missing focus-visible ring
|
|
89
|
+
pages/login.tsx:42 - [FORMS] Input missing autocomplete attribute
|
|
90
|
+
```
|
|
91
|
+
|
|
92
|
+
---
|
|
93
|
+
|
|
94
|
+
> **Remember:** Run anti-patterns check first for quick wins.
|
|
@@ -0,0 +1,153 @@
|
|
|
1
|
+
# Animation Guidelines
|
|
2
|
+
|
|
3
|
+
> Respect user preferences. Animate only what's necessary.
|
|
4
|
+
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
## Reduced Motion
|
|
8
|
+
|
|
9
|
+
**Honor `prefers-reduced-motion`.**
|
|
10
|
+
|
|
11
|
+
```css
|
|
12
|
+
/* ✅ Default animation */
|
|
13
|
+
.card {
|
|
14
|
+
transition: transform 200ms ease;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
/* ✅ Reduce or disable for preference */
|
|
18
|
+
@media (prefers-reduced-motion: reduce) {
|
|
19
|
+
.card {
|
|
20
|
+
transition: none;
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
```
|
|
24
|
+
|
|
25
|
+
In Tailwind:
|
|
26
|
+
```html
|
|
27
|
+
<div class="motion-safe:animate-bounce motion-reduce:animate-none">
|
|
28
|
+
```
|
|
29
|
+
|
|
30
|
+
---
|
|
31
|
+
|
|
32
|
+
## Compositor-Friendly Properties
|
|
33
|
+
|
|
34
|
+
**Animate only `transform` and `opacity`.**
|
|
35
|
+
|
|
36
|
+
| ✅ Animate | ❌ Avoid |
|
|
37
|
+
|-----------|---------|
|
|
38
|
+
| `transform` | `width`, `height` |
|
|
39
|
+
| `opacity` | `top`, `left` |
|
|
40
|
+
| `filter` | `margin`, `padding` |
|
|
41
|
+
| | `border-width` |
|
|
42
|
+
|
|
43
|
+
```css
|
|
44
|
+
/* ✅ GOOD: GPU-accelerated */
|
|
45
|
+
.card:hover {
|
|
46
|
+
transform: translateY(-4px);
|
|
47
|
+
opacity: 0.9;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/* ❌ BAD: Triggers layout */
|
|
51
|
+
.card:hover {
|
|
52
|
+
margin-top: -4px;
|
|
53
|
+
height: 104px;
|
|
54
|
+
}
|
|
55
|
+
```
|
|
56
|
+
|
|
57
|
+
---
|
|
58
|
+
|
|
59
|
+
## Explicit Transitions
|
|
60
|
+
|
|
61
|
+
**Never use `transition: all`.**
|
|
62
|
+
|
|
63
|
+
```css
|
|
64
|
+
/* ❌ BAD: Animates everything */
|
|
65
|
+
button {
|
|
66
|
+
transition: all 200ms;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/* ✅ GOOD: Explicit properties */
|
|
70
|
+
button {
|
|
71
|
+
transition: background-color 200ms, transform 150ms;
|
|
72
|
+
}
|
|
73
|
+
```
|
|
74
|
+
|
|
75
|
+
---
|
|
76
|
+
|
|
77
|
+
## Transform Origin
|
|
78
|
+
|
|
79
|
+
**Set correct origin for expected behavior.**
|
|
80
|
+
|
|
81
|
+
```css
|
|
82
|
+
/* ✅ Scale from center (default) */
|
|
83
|
+
.modal {
|
|
84
|
+
transform-origin: center;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
/* ✅ Dropdown from top */
|
|
88
|
+
.dropdown {
|
|
89
|
+
transform-origin: top;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
/* ✅ Menu from corner */
|
|
93
|
+
.context-menu {
|
|
94
|
+
transform-origin: top left;
|
|
95
|
+
}
|
|
96
|
+
```
|
|
97
|
+
|
|
98
|
+
---
|
|
99
|
+
|
|
100
|
+
## SVG Animation
|
|
101
|
+
|
|
102
|
+
**Animate wrapper div, not SVG element.**
|
|
103
|
+
|
|
104
|
+
```tsx
|
|
105
|
+
// ✅ GOOD: Animate wrapper
|
|
106
|
+
<div className="animate-spin">
|
|
107
|
+
<SpinnerSVG />
|
|
108
|
+
</div>
|
|
109
|
+
|
|
110
|
+
// ❌ BAD: Animate SVG directly (inconsistent behavior)
|
|
111
|
+
<SpinnerSVG className="animate-spin" />
|
|
112
|
+
```
|
|
113
|
+
|
|
114
|
+
For SVG transforms:
|
|
115
|
+
```css
|
|
116
|
+
.svg-icon {
|
|
117
|
+
transform-box: fill-box;
|
|
118
|
+
transform-origin: center;
|
|
119
|
+
}
|
|
120
|
+
```
|
|
121
|
+
|
|
122
|
+
---
|
|
123
|
+
|
|
124
|
+
## Interruptible Animations
|
|
125
|
+
|
|
126
|
+
**Animations should respond to user input.**
|
|
127
|
+
|
|
128
|
+
```css
|
|
129
|
+
/* ✅ Allow interruption */
|
|
130
|
+
.drawer {
|
|
131
|
+
transition: transform 300ms ease-out;
|
|
132
|
+
will-change: transform;
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
/* User can swipe to close mid-animation */
|
|
136
|
+
```
|
|
137
|
+
|
|
138
|
+
Avoid:
|
|
139
|
+
- Long animations (>500ms) without skip
|
|
140
|
+
- Animations that block interaction
|
|
141
|
+
- Auto-play that can't be paused
|
|
142
|
+
|
|
143
|
+
---
|
|
144
|
+
|
|
145
|
+
## Quick Checklist
|
|
146
|
+
|
|
147
|
+
| Check | Pass |
|
|
148
|
+
|-------|------|
|
|
149
|
+
| `prefers-reduced-motion` respected? | |
|
|
150
|
+
| Only `transform`/`opacity` animated? | |
|
|
151
|
+
| No `transition: all`? | |
|
|
152
|
+
| Correct `transform-origin`? | |
|
|
153
|
+
| SVG wrapped for animation? | |
|
|
@@ -0,0 +1,204 @@
|
|
|
1
|
+
# Anti-Patterns (FLAG THESE)
|
|
2
|
+
|
|
3
|
+
> Explicit list of violations to flag during UI audits.
|
|
4
|
+
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
## ❌ Accessibility Violations
|
|
8
|
+
|
|
9
|
+
### Zoom Blocking
|
|
10
|
+
```html
|
|
11
|
+
<!-- ❌ FLAG: Disables zoom -->
|
|
12
|
+
<meta name="viewport" content="user-scalable=no">
|
|
13
|
+
<meta name="viewport" content="maximum-scale=1">
|
|
14
|
+
|
|
15
|
+
<!-- ✅ CORRECT -->
|
|
16
|
+
<meta name="viewport" content="width=device-width, initial-scale=1">
|
|
17
|
+
```
|
|
18
|
+
|
|
19
|
+
### Outline Removal Without Replacement
|
|
20
|
+
```css
|
|
21
|
+
/* ❌ FLAG */
|
|
22
|
+
button:focus {
|
|
23
|
+
outline: none;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/* ✅ CORRECT */
|
|
27
|
+
button:focus-visible {
|
|
28
|
+
outline: 2px solid var(--focus);
|
|
29
|
+
}
|
|
30
|
+
```
|
|
31
|
+
|
|
32
|
+
### Missing Labels
|
|
33
|
+
```tsx
|
|
34
|
+
// ❌ FLAG: Input without label
|
|
35
|
+
<input type="email" />
|
|
36
|
+
|
|
37
|
+
// ❌ FLAG: Icon button without aria-label
|
|
38
|
+
<button><IconMenu /></button>
|
|
39
|
+
|
|
40
|
+
// ✅ CORRECT
|
|
41
|
+
<label htmlFor="email">Email</label>
|
|
42
|
+
<input id="email" type="email" />
|
|
43
|
+
|
|
44
|
+
<button aria-label="Open menu"><IconMenu /></button>
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
---
|
|
48
|
+
|
|
49
|
+
## ❌ Semantic Violations
|
|
50
|
+
|
|
51
|
+
### Wrong Element for Interaction
|
|
52
|
+
```tsx
|
|
53
|
+
// ❌ FLAG: Div with click handler
|
|
54
|
+
<div onClick={handleClick}>Click me</div>
|
|
55
|
+
<span onClick={handleClick}>Action</span>
|
|
56
|
+
|
|
57
|
+
// ✅ CORRECT
|
|
58
|
+
<button onClick={handleClick}>Click me</button>
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
### Inline Navigation Without Link
|
|
62
|
+
```tsx
|
|
63
|
+
// ❌ FLAG: onClick navigation
|
|
64
|
+
<button onClick={() => router.push('/dashboard')}>
|
|
65
|
+
Dashboard
|
|
66
|
+
</button>
|
|
67
|
+
|
|
68
|
+
// ✅ CORRECT
|
|
69
|
+
<Link href="/dashboard">Dashboard</Link>
|
|
70
|
+
```
|
|
71
|
+
|
|
72
|
+
---
|
|
73
|
+
|
|
74
|
+
## ❌ Form Violations
|
|
75
|
+
|
|
76
|
+
### Paste Blocking
|
|
77
|
+
```tsx
|
|
78
|
+
// ❌ FLAG: Blocks paste
|
|
79
|
+
<input onPaste={e => e.preventDefault()} />
|
|
80
|
+
<input onPaste={() => false} />
|
|
81
|
+
|
|
82
|
+
// ✅ CORRECT: Allow paste always
|
|
83
|
+
<input type="password" />
|
|
84
|
+
```
|
|
85
|
+
|
|
86
|
+
### Missing Autocomplete
|
|
87
|
+
```tsx
|
|
88
|
+
// ❌ FLAG: No autocomplete on auth fields
|
|
89
|
+
<input type="email" name="email" />
|
|
90
|
+
|
|
91
|
+
// ✅ CORRECT
|
|
92
|
+
<input type="email" name="email" autoComplete="email" />
|
|
93
|
+
```
|
|
94
|
+
|
|
95
|
+
---
|
|
96
|
+
|
|
97
|
+
## ❌ Animation Violations
|
|
98
|
+
|
|
99
|
+
### Transition All
|
|
100
|
+
```css
|
|
101
|
+
/* ❌ FLAG */
|
|
102
|
+
.button {
|
|
103
|
+
transition: all 200ms;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
/* ✅ CORRECT */
|
|
107
|
+
.button {
|
|
108
|
+
transition: background-color 200ms, transform 150ms;
|
|
109
|
+
}
|
|
110
|
+
```
|
|
111
|
+
|
|
112
|
+
### Layout Property Animation
|
|
113
|
+
```css
|
|
114
|
+
/* ❌ FLAG: Animates layout properties */
|
|
115
|
+
.card {
|
|
116
|
+
transition: width 200ms, height 200ms, margin 200ms;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
/* ✅ CORRECT: Transform only */
|
|
120
|
+
.card {
|
|
121
|
+
transition: transform 200ms;
|
|
122
|
+
}
|
|
123
|
+
```
|
|
124
|
+
|
|
125
|
+
---
|
|
126
|
+
|
|
127
|
+
## ❌ Performance Violations
|
|
128
|
+
|
|
129
|
+
### Large List Without Virtualization
|
|
130
|
+
```tsx
|
|
131
|
+
// ❌ FLAG: 50+ items without virtualization
|
|
132
|
+
{items.map(item => <Item key={item.id} {...item} />)}
|
|
133
|
+
// items.length > 50
|
|
134
|
+
|
|
135
|
+
// ✅ CORRECT: Use virtualization
|
|
136
|
+
import { Virtualizer } from '@tanstack/virtual-core';
|
|
137
|
+
```
|
|
138
|
+
|
|
139
|
+
### Images Without Dimensions
|
|
140
|
+
```tsx
|
|
141
|
+
// ❌ FLAG: No width/height
|
|
142
|
+
<img src="/photo.jpg" />
|
|
143
|
+
|
|
144
|
+
// ✅ CORRECT
|
|
145
|
+
<img src="/photo.jpg" width={800} height={600} alt="Description" />
|
|
146
|
+
```
|
|
147
|
+
|
|
148
|
+
### Hardcoded Date/Number Formats
|
|
149
|
+
```tsx
|
|
150
|
+
// ❌ FLAG
|
|
151
|
+
const date = `${d.getMonth()}/${d.getDate()}/${d.getFullYear()}`;
|
|
152
|
+
const price = `$${amount.toFixed(2)}`;
|
|
153
|
+
|
|
154
|
+
// ✅ CORRECT
|
|
155
|
+
const date = new Intl.DateTimeFormat('en-US').format(d);
|
|
156
|
+
const price = new Intl.NumberFormat('en-US', {
|
|
157
|
+
style: 'currency',
|
|
158
|
+
currency: 'USD'
|
|
159
|
+
}).format(amount);
|
|
160
|
+
```
|
|
161
|
+
|
|
162
|
+
---
|
|
163
|
+
|
|
164
|
+
## ❌ Mobile Violations
|
|
165
|
+
|
|
166
|
+
### AutoFocus on Mobile
|
|
167
|
+
```tsx
|
|
168
|
+
// ❌ FLAG: AutoFocus without mobile check
|
|
169
|
+
<input autoFocus />
|
|
170
|
+
|
|
171
|
+
// ✅ CORRECT
|
|
172
|
+
<input autoFocus={!isMobile} />
|
|
173
|
+
```
|
|
174
|
+
|
|
175
|
+
### Missing Safe Areas
|
|
176
|
+
```css
|
|
177
|
+
/* ❌ FLAG: Full-bleed without safe areas */
|
|
178
|
+
.full-width {
|
|
179
|
+
padding: 0;
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
/* ✅ CORRECT */
|
|
183
|
+
.full-width {
|
|
184
|
+
padding-left: env(safe-area-inset-left);
|
|
185
|
+
padding-right: env(safe-area-inset-right);
|
|
186
|
+
}
|
|
187
|
+
```
|
|
188
|
+
|
|
189
|
+
---
|
|
190
|
+
|
|
191
|
+
## Audit Checklist
|
|
192
|
+
|
|
193
|
+
| Category | Check | Severity |
|
|
194
|
+
|----------|-------|----------|
|
|
195
|
+
| Zoom | No `user-scalable=no` | 🔴 Critical |
|
|
196
|
+
| Focus | Visible focus states | 🔴 Critical |
|
|
197
|
+
| Labels | All inputs labeled | 🔴 Critical |
|
|
198
|
+
| Semantics | `<button>` for actions | 🟠 High |
|
|
199
|
+
| Links | `<Link>` for navigation | 🟠 High |
|
|
200
|
+
| Paste | Never blocked | 🟠 High |
|
|
201
|
+
| Transition | No `transition: all` | 🟡 Medium |
|
|
202
|
+
| Images | Has dimensions | 🟡 Medium |
|
|
203
|
+
| Lists | Virtualized if 50+ | 🟡 Medium |
|
|
204
|
+
| Dates | Uses `Intl.*` | 🟢 Low |
|
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
# Focus States Guidelines
|
|
2
|
+
|
|
3
|
+
> Keyboard users rely on visible focus. Never hide it without replacement.
|
|
4
|
+
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
## Visible Focus
|
|
8
|
+
|
|
9
|
+
**Interactive elements need visible focus.**
|
|
10
|
+
|
|
11
|
+
```css
|
|
12
|
+
/* ✅ Focus-visible (keyboard only) */
|
|
13
|
+
button:focus-visible {
|
|
14
|
+
outline: 2px solid var(--focus-ring);
|
|
15
|
+
outline-offset: 2px;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
/* ❌ Never do this without replacement */
|
|
19
|
+
button:focus {
|
|
20
|
+
outline: none;
|
|
21
|
+
}
|
|
22
|
+
```
|
|
23
|
+
|
|
24
|
+
| Tailwind | CSS |
|
|
25
|
+
|----------|-----|
|
|
26
|
+
| `focus-visible:ring-2` | `outline: 2px solid` |
|
|
27
|
+
| `focus-visible:ring-offset-2` | `outline-offset: 2px` |
|
|
28
|
+
| `focus-visible:ring-blue-500` | Custom color |
|
|
29
|
+
|
|
30
|
+
---
|
|
31
|
+
|
|
32
|
+
## Focus-Visible vs Focus
|
|
33
|
+
|
|
34
|
+
**Use `:focus-visible` over `:focus`.**
|
|
35
|
+
|
|
36
|
+
| Selector | When Applied |
|
|
37
|
+
|----------|--------------|
|
|
38
|
+
| `:focus` | Always on focus (keyboard + click) |
|
|
39
|
+
| `:focus-visible` | Only on keyboard focus |
|
|
40
|
+
|
|
41
|
+
```css
|
|
42
|
+
/* ✅ Only shows ring on keyboard navigation */
|
|
43
|
+
button:focus-visible {
|
|
44
|
+
ring: 2px;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/* ❌ Shows ring on every click too */
|
|
48
|
+
button:focus {
|
|
49
|
+
ring: 2px;
|
|
50
|
+
}
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
---
|
|
54
|
+
|
|
55
|
+
## Focus-Within
|
|
56
|
+
|
|
57
|
+
**Group focus with `:focus-within` for compound controls.**
|
|
58
|
+
|
|
59
|
+
```css
|
|
60
|
+
/* ✅ Highlight container when any child is focused */
|
|
61
|
+
.search-box:focus-within {
|
|
62
|
+
border-color: var(--focus);
|
|
63
|
+
box-shadow: 0 0 0 3px var(--focus-ring);
|
|
64
|
+
}
|
|
65
|
+
```
|
|
66
|
+
|
|
67
|
+
Use cases:
|
|
68
|
+
- Search box with button
|
|
69
|
+
- Input groups
|
|
70
|
+
- Custom selects
|
|
71
|
+
- Dropdown menus
|
|
72
|
+
|
|
73
|
+
---
|
|
74
|
+
|
|
75
|
+
## Tab Order
|
|
76
|
+
|
|
77
|
+
| Rule | Check |
|
|
78
|
+
|------|-------|
|
|
79
|
+
| Logical order | Tab follows visual layout |
|
|
80
|
+
| No positive tabindex | Use `tabindex="0"` or `-1` only |
|
|
81
|
+
| Skip links | Provide "Skip to content" |
|
|
82
|
+
| Focus trapping | Modal keeps focus inside |
|
|
83
|
+
|
|
84
|
+
```tsx
|
|
85
|
+
// ✅ Focusable div (rare cases)
|
|
86
|
+
<div tabIndex={0} role="button" onKeyDown={handleKey}>
|
|
87
|
+
Custom control
|
|
88
|
+
</div>
|
|
89
|
+
|
|
90
|
+
// ❌ Avoid
|
|
91
|
+
<div tabIndex={5}> // Breaks natural order
|
|
92
|
+
```
|
|
93
|
+
|
|
94
|
+
---
|
|
95
|
+
|
|
96
|
+
## Quick Audit
|
|
97
|
+
|
|
98
|
+
| Check | Pass/Fail |
|
|
99
|
+
|-------|-----------|
|
|
100
|
+
| Can tab through all interactive elements? | |
|
|
101
|
+
| Focus ring visible on each? | |
|
|
102
|
+
| No orphaned focus (hidden elements)? | |
|
|
103
|
+
| Modal traps focus? | |
|
|
104
|
+
| Escape closes modal? | |
|
|
@@ -0,0 +1,154 @@
|
|
|
1
|
+
# Forms Guidelines
|
|
2
|
+
|
|
3
|
+
> Form inputs are critical for user experience and security.
|
|
4
|
+
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
## Autocomplete
|
|
8
|
+
|
|
9
|
+
**Inputs need `autocomplete` and meaningful `name`.**
|
|
10
|
+
|
|
11
|
+
```tsx
|
|
12
|
+
// ✅ GOOD
|
|
13
|
+
<input
|
|
14
|
+
type="email"
|
|
15
|
+
name="email"
|
|
16
|
+
autoComplete="email"
|
|
17
|
+
/>
|
|
18
|
+
|
|
19
|
+
// ❌ BAD
|
|
20
|
+
<input type="text" /> // No type, no autocomplete
|
|
21
|
+
```
|
|
22
|
+
|
|
23
|
+
| Field | autocomplete Value |
|
|
24
|
+
|-------|-------------------|
|
|
25
|
+
| Email | `email` |
|
|
26
|
+
| Password | `current-password` or `new-password` |
|
|
27
|
+
| Name | `name` |
|
|
28
|
+
| Phone | `tel` |
|
|
29
|
+
| Address | `street-address` |
|
|
30
|
+
| Credit Card | `cc-number` |
|
|
31
|
+
|
|
32
|
+
---
|
|
33
|
+
|
|
34
|
+
## Input Types
|
|
35
|
+
|
|
36
|
+
**Use correct `type` and `inputmode`.**
|
|
37
|
+
|
|
38
|
+
| Data | type | inputmode |
|
|
39
|
+
|------|------|-----------|
|
|
40
|
+
| Email | `email` | `email` |
|
|
41
|
+
| Phone | `tel` | `tel` |
|
|
42
|
+
| URL | `url` | `url` |
|
|
43
|
+
| Number | `number` | `numeric` |
|
|
44
|
+
| Search | `search` | `search` |
|
|
45
|
+
| OTP/Code | `text` | `numeric` |
|
|
46
|
+
|
|
47
|
+
---
|
|
48
|
+
|
|
49
|
+
## Never Block Paste
|
|
50
|
+
|
|
51
|
+
```tsx
|
|
52
|
+
// ❌ NEVER DO THIS
|
|
53
|
+
<input
|
|
54
|
+
onPaste={e => e.preventDefault()} // Blocks password managers!
|
|
55
|
+
/>
|
|
56
|
+
|
|
57
|
+
// ✅ Always allow paste
|
|
58
|
+
<input type="password" />
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
---
|
|
62
|
+
|
|
63
|
+
## Labels
|
|
64
|
+
|
|
65
|
+
**Labels must be clickable.**
|
|
66
|
+
|
|
67
|
+
```tsx
|
|
68
|
+
// ✅ Using htmlFor
|
|
69
|
+
<label htmlFor="email">Email</label>
|
|
70
|
+
<input id="email" />
|
|
71
|
+
|
|
72
|
+
// ✅ Wrapping
|
|
73
|
+
<label>
|
|
74
|
+
Email
|
|
75
|
+
<input />
|
|
76
|
+
</label>
|
|
77
|
+
|
|
78
|
+
// ❌ No association
|
|
79
|
+
<span>Email</span>
|
|
80
|
+
<input />
|
|
81
|
+
```
|
|
82
|
+
|
|
83
|
+
---
|
|
84
|
+
|
|
85
|
+
## Error Handling
|
|
86
|
+
|
|
87
|
+
| Rule | Implementation |
|
|
88
|
+
|------|----------------|
|
|
89
|
+
| Inline errors | Next to field, not just top of form |
|
|
90
|
+
| Focus first error | On submit, focus the first invalid field |
|
|
91
|
+
| Clear on fix | Remove error when user starts typing |
|
|
92
|
+
| Specific messages | "Email is invalid" not "Error" |
|
|
93
|
+
|
|
94
|
+
```tsx
|
|
95
|
+
// ✅ Inline error
|
|
96
|
+
<input aria-invalid={!!error} aria-describedby="email-error" />
|
|
97
|
+
{error && <span id="email-error">{error}</span>}
|
|
98
|
+
```
|
|
99
|
+
|
|
100
|
+
---
|
|
101
|
+
|
|
102
|
+
## Submit Button
|
|
103
|
+
|
|
104
|
+
| State | Button |
|
|
105
|
+
|-------|--------|
|
|
106
|
+
| Idle | Enabled, shows action |
|
|
107
|
+
| Submitting | Shows spinner, may disable |
|
|
108
|
+
| Error | Returns to idle |
|
|
109
|
+
| Success | Shows confirmation |
|
|
110
|
+
|
|
111
|
+
```tsx
|
|
112
|
+
<button disabled={isSubmitting}>
|
|
113
|
+
{isSubmitting ? <Spinner /> : 'Submit'}
|
|
114
|
+
</button>
|
|
115
|
+
```
|
|
116
|
+
|
|
117
|
+
---
|
|
118
|
+
|
|
119
|
+
## Spellcheck
|
|
120
|
+
|
|
121
|
+
**Disable on non-prose fields.**
|
|
122
|
+
|
|
123
|
+
```tsx
|
|
124
|
+
<input
|
|
125
|
+
type="email"
|
|
126
|
+
spellCheck={false} // ✅ No squiggly lines on emails
|
|
127
|
+
autoComplete="off" // ✅ No password manager on non-auth
|
|
128
|
+
/>
|
|
129
|
+
```
|
|
130
|
+
|
|
131
|
+
| Disable spellcheck | Keep spellcheck |
|
|
132
|
+
|-------------------|-----------------|
|
|
133
|
+
| Email, username | Comments, bio |
|
|
134
|
+
| Codes, tokens | Messages |
|
|
135
|
+
| URLs | Search queries |
|
|
136
|
+
|
|
137
|
+
---
|
|
138
|
+
|
|
139
|
+
## Unsaved Changes Warning
|
|
140
|
+
|
|
141
|
+
**Warn before navigation with unsaved changes.**
|
|
142
|
+
|
|
143
|
+
```tsx
|
|
144
|
+
useEffect(() => {
|
|
145
|
+
const handleBeforeUnload = (e: BeforeUnloadEvent) => {
|
|
146
|
+
if (isDirty) {
|
|
147
|
+
e.preventDefault();
|
|
148
|
+
e.returnValue = '';
|
|
149
|
+
}
|
|
150
|
+
};
|
|
151
|
+
window.addEventListener('beforeunload', handleBeforeUnload);
|
|
152
|
+
return () => window.removeEventListener('beforeunload', handleBeforeUnload);
|
|
153
|
+
}, [isDirty]);
|
|
154
|
+
```
|