opencodekit 0.18.9 → 0.18.11
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/dist/index.js +1 -1
- package/dist/template/.opencode/AGENTS.md +11 -0
- package/dist/template/.opencode/agent/build.md +1 -1
- package/dist/template/.opencode/agent/general.md +1 -1
- package/dist/template/.opencode/command/ui-slop-check.md +146 -0
- package/dist/template/.opencode/memory.db +0 -0
- package/dist/template/.opencode/memory.db-shm +0 -0
- package/dist/template/.opencode/memory.db-wal +0 -0
- package/dist/template/.opencode/package.json +1 -1
- package/dist/template/.opencode/plugin/lib/context.ts +9 -5
- package/dist/template/.opencode/plugin/lib/db/types.ts +1 -1
- package/dist/template/.opencode/plugin/lib/memory-hooks.ts +17 -6
- package/dist/template/.opencode/skill/frontend-design/SKILL.md +114 -44
- package/dist/template/.opencode/skill/frontend-design/references/animation/motion-core.md +118 -108
- package/dist/template/.opencode/skill/frontend-design/references/design/color-system.md +111 -0
- package/dist/template/.opencode/skill/frontend-design/references/design/interaction.md +149 -0
- package/dist/template/.opencode/skill/frontend-design/references/design/typography-rules.md +106 -0
- package/dist/template/.opencode/skill/frontend-design/references/design/ux-writing.md +99 -0
- package/package.json +1 -1
- package/dist/template/.opencode/memory/memory.db +0 -0
package/dist/index.js
CHANGED
|
@@ -306,6 +306,17 @@ For major tracked work:
|
|
|
306
306
|
4. **EDIT** — Include 2-3 unique context lines before/after
|
|
307
307
|
5. **CONFIRM** — Read back to verify edit succeeded
|
|
308
308
|
|
|
309
|
+
### Write Tool Safety (Runtime Guard)
|
|
310
|
+
|
|
311
|
+
OpenCode enforces a **hard runtime check**: you must Read a file before Writing to it. This is not a prompt suggestion — it's a `FileTime.assert()` call that throws if no read timestamp exists for the file in the current session.
|
|
312
|
+
|
|
313
|
+
- **Existing files**: Always `Read` before `Write`. The Write tool will reject overwrites without a prior Read.
|
|
314
|
+
- **New files**: Write freely — the guard only fires for files that already exist.
|
|
315
|
+
- **Edit tool**: Same guard applies. Read first, then Edit.
|
|
316
|
+
- **Failure**: `"You must read file X before overwriting it. Use the Read tool first"`
|
|
317
|
+
|
|
318
|
+
**Rule**: Never use Write on an existing file without Reading it first in the same session. Prefer Edit for modifications; reserve Write for new file creation or full replacements after Read.
|
|
319
|
+
|
|
309
320
|
### File Size Guidance
|
|
310
321
|
|
|
311
322
|
Files over ~500 lines become hard to maintain and review. Extract helpers, split modules, or refactor when approaching this threshold.
|
|
@@ -72,7 +72,7 @@ Implement requested work, verify with fresh evidence, and coordinate subagents o
|
|
|
72
72
|
### Scope Discipline
|
|
73
73
|
|
|
74
74
|
- Stay in scope; no speculative refactors or bonus features
|
|
75
|
-
- Read files before editing
|
|
75
|
+
- **Read files before editing or writing** — Write tool rejects overwrites without a prior Read (runtime guard)
|
|
76
76
|
- Delegate when work is large, uncertain, or cross-domain
|
|
77
77
|
|
|
78
78
|
### Verification as Calibration
|
|
@@ -52,7 +52,7 @@ Execute clear, low-complexity coding tasks quickly (typically 1-3 files) and rep
|
|
|
52
52
|
|
|
53
53
|
## Rules
|
|
54
54
|
|
|
55
|
-
- Read
|
|
55
|
+
- **Read before editing or writing** — Write/Edit tools reject changes to existing files without a prior Read (runtime guard)
|
|
56
56
|
- Keep changes minimal and in-scope
|
|
57
57
|
- Ask before irreversible actions (commit, push, destructive ops)
|
|
58
58
|
|
|
@@ -0,0 +1,146 @@
|
|
|
1
|
+
---
|
|
2
|
+
description: Audit changed UI files for AI slop patterns and design-system violations
|
|
3
|
+
argument-hint: "[path|auto] [--staged] [--since=<ref>] [--full-report]"
|
|
4
|
+
agent: vision
|
|
5
|
+
model: proxypal/gemini-3-pro-preview
|
|
6
|
+
---
|
|
7
|
+
|
|
8
|
+
# UI Slop Check: $ARGUMENTS
|
|
9
|
+
|
|
10
|
+
Run a focused anti-slop audit against changed UI files using the frontend-design taxonomy.
|
|
11
|
+
|
|
12
|
+
## Load Skills
|
|
13
|
+
|
|
14
|
+
```typescript
|
|
15
|
+
skill({ name: "frontend-design" }); // Anti-pattern taxonomy + design references
|
|
16
|
+
skill({ name: "visual-analysis" }); // Structured visual/code analysis workflow
|
|
17
|
+
skill({ name: "accessibility-audit" }); // Keyboard/focus/contrast checks
|
|
18
|
+
```
|
|
19
|
+
|
|
20
|
+
## Parse Arguments
|
|
21
|
+
|
|
22
|
+
| Argument | Default | Description |
|
|
23
|
+
| --------------- | ------- | -------------------------------------------------- | ----------------------------------------------------------- |
|
|
24
|
+
| `[path | auto]` | `auto` | Specific file/dir to audit, or auto-detect changed UI files |
|
|
25
|
+
| `--staged` | false | Audit staged changes only (`git diff --cached`) |
|
|
26
|
+
| `--since=<ref>` | `HEAD` | Compare against ref (`main`, `HEAD~1`, commit SHA) |
|
|
27
|
+
| `--full-report` | false | Include all categories even when no issues found |
|
|
28
|
+
|
|
29
|
+
## Phase 1: Resolve Target Files
|
|
30
|
+
|
|
31
|
+
If `[path]` is provided:
|
|
32
|
+
|
|
33
|
+
- Audit that path directly
|
|
34
|
+
|
|
35
|
+
If `auto`:
|
|
36
|
+
|
|
37
|
+
```bash
|
|
38
|
+
# unstaged + staged by default
|
|
39
|
+
git diff --name-only $SINCE_REF -- \
|
|
40
|
+
'*.tsx' '*.jsx' '*.css' '*.scss' '*.sass' '*.less' '*.html' '*.mdx'
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
If `--staged`:
|
|
44
|
+
|
|
45
|
+
```bash
|
|
46
|
+
git diff --cached --name-only -- \
|
|
47
|
+
'*.tsx' '*.jsx' '*.css' '*.scss' '*.sass' '*.less' '*.html' '*.mdx'
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
Prioritize files under:
|
|
51
|
+
|
|
52
|
+
- `src/components/**`
|
|
53
|
+
- `src/app/**`
|
|
54
|
+
- `src/pages/**`
|
|
55
|
+
- `app/**`
|
|
56
|
+
- `components/**`
|
|
57
|
+
|
|
58
|
+
If no UI files changed, return: **PASS (no changed UI files)**.
|
|
59
|
+
|
|
60
|
+
## Phase 2: Run AI Slop Checklist
|
|
61
|
+
|
|
62
|
+
Evaluate each target file (or rendered screenshot if provided) against these checks.
|
|
63
|
+
|
|
64
|
+
### A) Typography
|
|
65
|
+
|
|
66
|
+
- Banned default aesthetics (Inter/Roboto/Arial/Open Sans as dominant display voice)
|
|
67
|
+
- Body text uses `rem/em`, not fixed `px`
|
|
68
|
+
- Clear hierarchy (size/weight/spacing), not color-only hierarchy
|
|
69
|
+
- Body line length near readable measure (around 65ch when applicable)
|
|
70
|
+
|
|
71
|
+
### B) Color and Theming
|
|
72
|
+
|
|
73
|
+
- No AI default palette tropes (purple-blue gradient defaults, neon-on-dark clichés)
|
|
74
|
+
- No pure `#000` / `#fff` as dominant surfaces
|
|
75
|
+
- Gray text is not placed on saturated backgrounds
|
|
76
|
+
- Semantic tokens are used (not random per-component hardcoded colors)
|
|
77
|
+
- Dark mode is adapted, not simple inversion
|
|
78
|
+
|
|
79
|
+
### C) Layout and Spatial Rhythm
|
|
80
|
+
|
|
81
|
+
- No cards-inside-cards without strong information architecture reason
|
|
82
|
+
- No repetitive cookie-cutter card blocks with identical structure
|
|
83
|
+
- Spacing rhythm is consistent (4pt-style cadence), not arbitrary jumps
|
|
84
|
+
- Uses `gap`/layout primitives cleanly; avoids margin hacks when possible
|
|
85
|
+
|
|
86
|
+
### D) Motion and Interaction
|
|
87
|
+
|
|
88
|
+
- No bounce/elastic gimmick motion for product UI
|
|
89
|
+
- Animations use transform/opacity (avoid layout-thrashing properties)
|
|
90
|
+
- Reduced motion support exists for meaningful motion
|
|
91
|
+
- States exist: hover, focus-visible, active, disabled, loading/error where relevant
|
|
92
|
+
|
|
93
|
+
### E) UX Writing
|
|
94
|
+
|
|
95
|
+
- Buttons are verb + object (e.g. "Save changes")
|
|
96
|
+
- Error copy includes what happened + why + how to fix
|
|
97
|
+
- Empty states include guidance + next action
|
|
98
|
+
- Terminology is consistent (avoid mixed synonyms for same action)
|
|
99
|
+
|
|
100
|
+
### F) Accessibility Safety Nets
|
|
101
|
+
|
|
102
|
+
- Keyboard-visible focus treatment (`:focus-visible`)
|
|
103
|
+
- Contrast baseline expectations (WCAG AA)
|
|
104
|
+
- Touch targets reasonable (44x44 context where applicable)
|
|
105
|
+
|
|
106
|
+
## Phase 3: Severity and Scoring
|
|
107
|
+
|
|
108
|
+
Group findings by severity:
|
|
109
|
+
|
|
110
|
+
- **Critical**: accessibility failures, broken interaction states, unreadable contrast
|
|
111
|
+
- **Warning**: strong AI fingerprint/slop patterns, inconsistent design system usage
|
|
112
|
+
- **Info**: polish/consistency opportunities
|
|
113
|
+
|
|
114
|
+
Score each category 1-10 and include evidence (`file:line` for code audits).
|
|
115
|
+
|
|
116
|
+
## Phase 4: Output
|
|
117
|
+
|
|
118
|
+
Return:
|
|
119
|
+
|
|
120
|
+
1. **Result**: PASS / NEEDS WORK
|
|
121
|
+
2. **Audited files** (list)
|
|
122
|
+
3. **Category scores**
|
|
123
|
+
4. **Findings by severity** with actionable fixes
|
|
124
|
+
5. **Fast remediation plan** (top 3 fixes first)
|
|
125
|
+
|
|
126
|
+
If `--full-report` is false, omit empty categories.
|
|
127
|
+
|
|
128
|
+
## Record Findings
|
|
129
|
+
|
|
130
|
+
```typescript
|
|
131
|
+
observation({
|
|
132
|
+
type: "warning",
|
|
133
|
+
title: "UI Slop Check: [scope]",
|
|
134
|
+
narrative: "Detected [count] critical, [count] warning slop issues in changed UI files.",
|
|
135
|
+
concepts: "ui, design, anti-patterns, frontend",
|
|
136
|
+
confidence: "high",
|
|
137
|
+
});
|
|
138
|
+
```
|
|
139
|
+
|
|
140
|
+
## Related Commands
|
|
141
|
+
|
|
142
|
+
| Need | Command |
|
|
143
|
+
| ---------------------------------------- | ------------ |
|
|
144
|
+
| Design from scratch | `/design` |
|
|
145
|
+
| Full UI review (single screen/component) | `/ui-review` |
|
|
146
|
+
| Implementation work | `/start` |
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
@@ -110,10 +110,14 @@ function compressMessage(msg: TransformMessage): TransformMessage {
|
|
|
110
110
|
* Keeps first and last sentence, truncates middle.
|
|
111
111
|
*/
|
|
112
112
|
function createSummary(text: string, role: string): string {
|
|
113
|
-
const maxChars =
|
|
113
|
+
const maxChars = 500;
|
|
114
114
|
|
|
115
115
|
if (text.length <= maxChars) return text;
|
|
116
116
|
|
|
117
|
+
// For very large messages (>5000 chars), be more aggressive
|
|
118
|
+
const isLarge = text.length > 5000;
|
|
119
|
+
const effectiveMax = isLarge ? 300 : maxChars;
|
|
120
|
+
|
|
117
121
|
// Split into sentences
|
|
118
122
|
const sentences = text
|
|
119
123
|
.split(/(?<=[.!?])\s+|\n+/)
|
|
@@ -121,16 +125,16 @@ function createSummary(text: string, role: string): string {
|
|
|
121
125
|
.filter((s) => s.length > 0);
|
|
122
126
|
|
|
123
127
|
if (sentences.length <= 2) {
|
|
124
|
-
return `${text.slice(0,
|
|
128
|
+
return `${text.slice(0, effectiveMax)}...`;
|
|
125
129
|
}
|
|
126
130
|
|
|
127
131
|
const first = sentences[0];
|
|
128
132
|
const last = sentences[sentences.length - 1];
|
|
129
133
|
|
|
130
|
-
const summary = `[compressed ${role}
|
|
134
|
+
const summary = `[compressed ${role}] ${first} [...${sentences.length - 2} sentences...] ${last}`;
|
|
131
135
|
|
|
132
|
-
return summary.length >
|
|
133
|
-
? `${summary.slice(0,
|
|
136
|
+
return summary.length > effectiveMax
|
|
137
|
+
? `${summary.slice(0, effectiveMax)}...`
|
|
134
138
|
: summary;
|
|
135
139
|
}
|
|
136
140
|
|
|
@@ -90,13 +90,24 @@ export function createHooks(deps: HookDeps) {
|
|
|
90
90
|
}
|
|
91
91
|
}
|
|
92
92
|
|
|
93
|
-
// --- Session error:
|
|
93
|
+
// --- Session error: show actual error details ---
|
|
94
94
|
if (event.type === "session.error") {
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
95
|
+
const props = event.properties as Record<string, unknown> | undefined;
|
|
96
|
+
const errorMsg = props?.error
|
|
97
|
+
? String(props.error).slice(0, 120)
|
|
98
|
+
: props?.message
|
|
99
|
+
? String(props.message).slice(0, 120)
|
|
100
|
+
: "Unknown error";
|
|
101
|
+
|
|
102
|
+
// Classify: match the specific AI SDK error pattern
|
|
103
|
+
const isTokenOverflow =
|
|
104
|
+
/token.{0,20}(exceed|limit)/i.test(errorMsg) ||
|
|
105
|
+
errorMsg.includes("context_length_exceeded");
|
|
106
|
+
const guidance = isTokenOverflow
|
|
107
|
+
? "Context too large — use /compact or start a new session"
|
|
108
|
+
: "Save important learnings with observation tool";
|
|
109
|
+
|
|
110
|
+
await showToast("Session Error", `${guidance} (${errorMsg})`, "warning");
|
|
100
111
|
}
|
|
101
112
|
},
|
|
102
113
|
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: frontend-design
|
|
3
3
|
description: Create distinctive, production-grade frontend interfaces. Use when building web components, pages, or applications with React-based frameworks. Includes Tailwind CSS v4, shadcn/ui components, Motion animations, and visual design philosophy for avoiding generic AI aesthetics.
|
|
4
|
-
version: 1.
|
|
4
|
+
version: 1.1.0
|
|
5
5
|
tags: [ui, design]
|
|
6
6
|
dependencies: []
|
|
7
7
|
---
|
|
@@ -21,7 +21,6 @@ dependencies: []
|
|
|
21
21
|
|
|
22
22
|
- Backend-only tasks or minimal UI with no visual design requirements.
|
|
23
23
|
|
|
24
|
-
|
|
25
24
|
## Reference Documentation
|
|
26
25
|
|
|
27
26
|
### Tailwind CSS v4.1
|
|
@@ -46,7 +45,7 @@ Search: `Field`, `InputGroup`, `Spinner`, `ButtonGroup`, `next-themes`
|
|
|
46
45
|
|
|
47
46
|
### Animation (Motion + Tailwind)
|
|
48
47
|
|
|
49
|
-
- `./references/animation/motion-core.md` -
|
|
48
|
+
- `./references/animation/motion-core.md` - Timing system, easing constants, performance rules, reduced motion, core patterns
|
|
50
49
|
- `./references/animation/motion-advanced.md` - AnimatePresence, scroll, orchestration, TypeScript
|
|
51
50
|
|
|
52
51
|
**Stack**:
|
|
@@ -64,6 +63,15 @@ Search: `Field`, `InputGroup`, `Spinner`, `ButtonGroup`, `next-themes`
|
|
|
64
63
|
|
|
65
64
|
For sophisticated compositions: posters, brand materials, design systems.
|
|
66
65
|
|
|
66
|
+
### Design Systems (Deep Guides)
|
|
67
|
+
|
|
68
|
+
- `./references/design/color-system.md` - OKLCH, semantic tokens, dark mode architecture
|
|
69
|
+
- `./references/design/typography-rules.md` - Fluid type, modular scale, OpenType features
|
|
70
|
+
- `./references/design/interaction.md` - State models, focus, dialogs/popovers, loading patterns
|
|
71
|
+
- `./references/design/ux-writing.md` - Button copy, error structure, empty states, i18n
|
|
72
|
+
|
|
73
|
+
Search: `AI Slop Test`, `tinted neutrals`, `focus-visible`, `verb + object`, `65ch`
|
|
74
|
+
|
|
67
75
|
## Design Thinking
|
|
68
76
|
|
|
69
77
|
Before coding, commit to BOLD aesthetic direction:
|
|
@@ -74,23 +82,60 @@ Before coding, commit to BOLD aesthetic direction:
|
|
|
74
82
|
|
|
75
83
|
Bold maximalism and refined minimalism both work. Key is intentionality.
|
|
76
84
|
|
|
77
|
-
## Anti-Patterns (NEVER)
|
|
85
|
+
## Anti-Patterns (AI Fingerprints — NEVER)
|
|
86
|
+
|
|
87
|
+
These patterns immediately signal "AI made this." Avoid them all.
|
|
88
|
+
|
|
89
|
+
### Typography
|
|
90
|
+
|
|
91
|
+
- **Banned fonts**: Inter, Roboto, Arial, Open Sans, Lato, Montserrat, Space Grotesk, system-ui as display font
|
|
92
|
+
- Monospace used as "developer aesthetic" shorthand
|
|
93
|
+
- Big icons centered above every heading
|
|
94
|
+
- Using `px` for body text (use `rem`/`em` to respect user settings)
|
|
95
|
+
|
|
96
|
+
### Color
|
|
78
97
|
|
|
79
|
-
-
|
|
80
|
-
-
|
|
81
|
-
-
|
|
82
|
-
-
|
|
83
|
-
-
|
|
98
|
+
- **AI palette**: Cyan-on-dark, purple-to-blue gradients, neon accents on dark backgrounds
|
|
99
|
+
- Gray text on colored backgrounds (use darker shade of background color instead)
|
|
100
|
+
- Pure `#000` or `#fff` (always tint — pure black/white don't exist in nature)
|
|
101
|
+
- Gradient text on headings or metrics
|
|
102
|
+
- `rgba()` / heavy alpha transparency as primary palette (design smell — define explicit colors)
|
|
103
|
+
|
|
104
|
+
### Layout
|
|
105
|
+
|
|
106
|
+
- Cards nested inside cards (use typography, spacing, dividers for hierarchy within a card)
|
|
107
|
+
- Identical card grids (icon + heading + text repeated 3-6 times)
|
|
108
|
+
- Hero metric template (big number + small label + gradient accent)
|
|
109
|
+
- Center-aligning everything
|
|
110
|
+
- Same spacing everywhere (no visual rhythm)
|
|
111
|
+
|
|
112
|
+
### Visual
|
|
113
|
+
|
|
114
|
+
- Glassmorphism used decoratively (blur cards, glow borders)
|
|
115
|
+
- Thick colored border on one side of rounded rectangles
|
|
116
|
+
- Sparklines as decoration (not connected to real data)
|
|
117
|
+
- Generic drop shadows on everything
|
|
118
|
+
- Rounded rectangles as the only shape language
|
|
119
|
+
|
|
120
|
+
### Motion
|
|
121
|
+
|
|
122
|
+
- Bounce or elastic easing (real objects decelerate smoothly)
|
|
123
|
+
- Animating `height`, `width`, `padding`, `margin` (causes layout recalculation)
|
|
124
|
+
- Default `ease` (compromise that's rarely optimal — use exponential easing)
|
|
125
|
+
- Missing `prefers-reduced-motion` handling
|
|
126
|
+
|
|
127
|
+
> **The AI Slop Test**: If you showed this interface to someone and said "AI made this," would they believe you immediately? If yes, that's the problem.
|
|
84
128
|
|
|
85
129
|
## Best Practices
|
|
86
130
|
|
|
87
|
-
1. **Accessibility First**: Radix primitives, focus
|
|
88
|
-
2. **Mobile-First**: Start mobile, layer responsive variants
|
|
89
|
-
3. **Design Tokens**:
|
|
90
|
-
4. **Dark Mode**:
|
|
91
|
-
5. **
|
|
92
|
-
6. **
|
|
93
|
-
7. **
|
|
131
|
+
1. **Accessibility First**: Radix primitives, `:focus-visible` (not `:focus`), semantic HTML, 44px touch targets
|
|
132
|
+
2. **Mobile-First**: Start mobile, layer responsive variants with `min-width` queries
|
|
133
|
+
3. **Design Tokens**: Two-layer system — primitives (`--blue-500`) + semantic (`--color-primary: var(--blue-500)`); dark mode redefines semantic layer only
|
|
134
|
+
4. **Dark Mode**: Not inverted light mode — lighter surfaces create depth (no shadows); desaturate accents; base at `oklch(15-18% …)`
|
|
135
|
+
5. **Container Queries**: Use `@container` for component layout, viewport queries for page layout
|
|
136
|
+
6. **Performance**: `transform` + `opacity` only for animations; CSS purging; avoid dynamic class names
|
|
137
|
+
7. **TypeScript**: Full type safety
|
|
138
|
+
8. **Expert Craftsmanship**: Every detail matters — squint test for hierarchy validation
|
|
94
139
|
|
|
95
140
|
## Core Stack Summary
|
|
96
141
|
|
|
@@ -102,63 +147,88 @@ Bold maximalism and refined minimalism both work. Key is intentionality.
|
|
|
102
147
|
|
|
103
148
|
## Typography
|
|
104
149
|
|
|
105
|
-
|
|
150
|
+
→ Consult `./references/design/typography-rules.md` for full fluid type system
|
|
151
|
+
|
|
152
|
+
Choose distinctive fonts. Pair display with body:
|
|
153
|
+
|
|
154
|
+
- **Preferred**: Instrument Sans, Plus Jakarta Sans, Outfit, Onest, Figtree, Urbanist (sans); Fraunces, Newsreader (editorial)
|
|
155
|
+
- **Fluid sizing**: `clamp(1rem, 0.5rem + 2vw, 1.5rem)` — never fixed `px` for body
|
|
156
|
+
- **Modular scale**: Pick one ratio (1.25 major third, 1.333 perfect fourth) — 5 sizes max
|
|
157
|
+
- **Measure**: `max-width: 65ch` for body text
|
|
158
|
+
- **OpenType**: `tabular-nums` for data, `diagonal-fractions` for recipes, `all-small-caps` for abbreviations
|
|
106
159
|
|
|
107
160
|
```css
|
|
108
161
|
@theme {
|
|
109
|
-
--font-display: "
|
|
110
|
-
--font-body: "
|
|
162
|
+
--font-display: "Fraunces", serif;
|
|
163
|
+
--font-body: "Instrument Sans", sans-serif;
|
|
111
164
|
}
|
|
112
165
|
```
|
|
113
166
|
|
|
114
167
|
## Color
|
|
115
168
|
|
|
116
|
-
|
|
169
|
+
→ Consult `./references/design/color-system.md` for OKLCH deep guide
|
|
170
|
+
|
|
171
|
+
Use OKLCH for perceptually uniform colors. Two-layer token system:
|
|
117
172
|
|
|
118
173
|
```css
|
|
119
174
|
@theme {
|
|
120
|
-
|
|
121
|
-
--
|
|
175
|
+
/* Primitives */
|
|
176
|
+
--blue-500: oklch(0.55 0.22 264);
|
|
177
|
+
--amber-400: oklch(0.75 0.18 80);
|
|
178
|
+
/* Semantic (redefine these for dark mode) */
|
|
179
|
+
--color-primary: var(--blue-500);
|
|
180
|
+
--color-accent: var(--amber-400);
|
|
181
|
+
/* Tinted neutrals — chroma 0.01 for subconscious brand cohesion */
|
|
182
|
+
--color-surface: oklch(0.97 0.01 264);
|
|
183
|
+
--color-surface-dark: oklch(0.16 0.01 264);
|
|
122
184
|
}
|
|
123
185
|
```
|
|
124
186
|
|
|
125
187
|
## Motion
|
|
126
188
|
|
|
127
|
-
|
|
189
|
+
→ Consult `./references/animation/motion-core.md` for Motion API
|
|
190
|
+
|
|
191
|
+
**Timing**: 100-150ms instant feedback | 200-300ms state changes | 300-500ms layout | exit = 75% of enter
|
|
192
|
+
|
|
193
|
+
**Easing**: Exponential only — `cubic-bezier(0.16, 1, 0.3, 1)` for entrances. Never `ease`, never bounce/elastic.
|
|
194
|
+
|
|
195
|
+
**Performance**: Only animate `transform` and `opacity`. Height expand → `grid-template-rows: 0fr → 1fr`.
|
|
196
|
+
|
|
197
|
+
**Accessibility**: `prefers-reduced-motion` is mandatory (affects ~35% of adults over 40). Swap spatial animations for crossfades.
|
|
128
198
|
|
|
129
199
|
```tsx
|
|
130
200
|
import { motion, AnimatePresence } from 'motion/react';
|
|
131
201
|
|
|
132
|
-
|
|
133
|
-
|
|
202
|
+
<motion.div initial={{ opacity: 0, y: 12 }} animate={{ opacity: 1, y: 0 }}
|
|
203
|
+
transition={{ duration: 0.3, ease: [0.16, 1, 0.3, 1] }} />
|
|
134
204
|
|
|
135
|
-
// Exit animations
|
|
136
205
|
<AnimatePresence>
|
|
137
|
-
{show && <motion.div exit={{ opacity: 0 }} />}
|
|
206
|
+
{show && <motion.div exit={{ opacity: 0 }} transition={{ duration: 0.2 }} />}
|
|
138
207
|
</AnimatePresence>
|
|
208
|
+
```
|
|
139
209
|
|
|
140
|
-
|
|
141
|
-
<motion.div layout />
|
|
210
|
+
CSS stagger: `animation-delay: calc(var(--i, 0) * 50ms)` — cap total at 500ms.
|
|
142
211
|
|
|
143
|
-
|
|
144
|
-
<motion.button whileHover={{ scale: 1.05 }} whileTap={{ scale: 0.95 }} />
|
|
145
|
-
```
|
|
212
|
+
## Spatial Composition
|
|
146
213
|
|
|
147
|
-
|
|
214
|
+
- **4pt spacing system**: 4, 8, 12, 16, 24, 32, 48, 64, 96px — not 8pt (too coarse at small gaps)
|
|
215
|
+
- **`gap` over margins** for sibling spacing (eliminates margin collapse)
|
|
216
|
+
- **Self-adjusting grids**: `repeat(auto-fit, minmax(280px, 1fr))` — responsive without breakpoints
|
|
217
|
+
- **Container queries** for components: `container-type: inline-size` on wrapper, `@container` on children
|
|
218
|
+
- **Optical text alignment**: `margin-left: -0.05em` for text that appears indented due to letterform whitespace
|
|
219
|
+
- Asymmetry, overlap, diagonal flow, grid-breaking elements. Generous negative space OR controlled density.
|
|
148
220
|
|
|
149
|
-
|
|
150
|
-
dialog[open] {
|
|
151
|
-
opacity: 1;
|
|
152
|
-
@starting-style {
|
|
153
|
-
opacity: 0;
|
|
154
|
-
transform: scale(0.95);
|
|
155
|
-
}
|
|
156
|
-
}
|
|
157
|
-
```
|
|
221
|
+
## Interaction
|
|
158
222
|
|
|
159
|
-
|
|
223
|
+
→ Consult `./references/design/interaction.md`
|
|
224
|
+
|
|
225
|
+
Design all 8 states: Default, Hover, Focus, Active, Disabled, Loading, Error, Success. Use `:focus-visible` not `:focus`. Native `<dialog>` + `inert` for modals. Popover API for tooltips/dropdowns. Skeleton screens over spinners. Undo over confirmation dialogs.
|
|
226
|
+
|
|
227
|
+
## UX Writing
|
|
228
|
+
|
|
229
|
+
→ Consult `./references/design/ux-writing.md`
|
|
160
230
|
|
|
161
|
-
|
|
231
|
+
Button labels: specific verb + object ("Save changes" not "OK"). Error formula: What happened + Why + How to fix. Empty states are onboarding opportunities. Plan for 30% text expansion (i18n).
|
|
162
232
|
|
|
163
233
|
## Backgrounds
|
|
164
234
|
|
|
@@ -1,171 +1,181 @@
|
|
|
1
|
-
# Motion Core (
|
|
1
|
+
# Motion Core (motion/react)
|
|
2
2
|
|
|
3
3
|
**Import**: `import { motion, AnimatePresence } from 'motion/react'`
|
|
4
4
|
|
|
5
|
-
##
|
|
5
|
+
## Motion Principles
|
|
6
6
|
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
animate={{ opacity: 1, y: 0 }}
|
|
12
|
-
transition={{ duration: 0.6 }}
|
|
13
|
-
/>
|
|
7
|
+
- Animate for clarity, not decoration
|
|
8
|
+
- Use motion to explain state change and hierarchy
|
|
9
|
+
- Prefer subtle distance (`8-16px`) and opacity shifts
|
|
10
|
+
- Use consistent timing/easing system across the app
|
|
14
11
|
|
|
15
|
-
|
|
16
|
-
<motion.div animate={{ scale: 1.1 }} />
|
|
17
|
-
```
|
|
12
|
+
## Timing System
|
|
18
13
|
|
|
19
|
-
|
|
14
|
+
| Use Case | Duration |
|
|
15
|
+
| ----------------------------- | ---------- |
|
|
16
|
+
| Instant feedback (hover/tap) | 100-150ms |
|
|
17
|
+
| State changes (menus/toggles) | 200-300ms |
|
|
18
|
+
| Layout transitions | 300-500ms |
|
|
19
|
+
| Large entrances | 500-800ms |
|
|
20
20
|
|
|
21
|
-
|
|
22
|
-
const variants = {
|
|
23
|
-
hidden: { opacity: 0, y: 20 },
|
|
24
|
-
visible: { opacity: 1, y: 0 }
|
|
25
|
-
};
|
|
21
|
+
**Rule**: exit duration = ~75% of enter duration.
|
|
26
22
|
|
|
27
|
-
|
|
28
|
-
variants={variants}
|
|
29
|
-
initial="hidden"
|
|
30
|
-
animate="visible"
|
|
31
|
-
/>
|
|
32
|
-
```
|
|
23
|
+
## Easing System
|
|
33
24
|
|
|
34
|
-
|
|
25
|
+
Use exponential easing by default:
|
|
35
26
|
|
|
36
27
|
```tsx
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
whileFocus={{ outline: '2px solid blue' }}
|
|
41
|
-
/>
|
|
28
|
+
const EASING_ENTER = [0.16, 1, 0.3, 1];
|
|
29
|
+
const EASING_EXIT = [0.4, 0, 1, 1];
|
|
30
|
+
```
|
|
42
31
|
|
|
43
|
-
|
|
32
|
+
Avoid bounce/elastic easings for product UI.
|
|
33
|
+
|
|
34
|
+
## Performance Rules
|
|
35
|
+
|
|
36
|
+
Animate only compositor-friendly properties:
|
|
37
|
+
|
|
38
|
+
- `transform`
|
|
39
|
+
- `opacity`
|
|
40
|
+
|
|
41
|
+
Avoid animating:
|
|
42
|
+
|
|
43
|
+
- `width`, `height`
|
|
44
|
+
- `top`, `left`
|
|
45
|
+
- `margin`, `padding`
|
|
46
|
+
|
|
47
|
+
## Basic Pattern
|
|
48
|
+
|
|
49
|
+
```tsx
|
|
44
50
|
<motion.div
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
51
|
+
initial={{ opacity: 0, y: 12 }}
|
|
52
|
+
animate={{ opacity: 1, y: 0 }}
|
|
53
|
+
transition={{ duration: 0.3, ease: [0.16, 1, 0.3, 1] }}
|
|
48
54
|
/>
|
|
49
55
|
```
|
|
50
56
|
|
|
51
|
-
##
|
|
57
|
+
## Variants Pattern (Recommended)
|
|
52
58
|
|
|
53
59
|
```tsx
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
60
|
+
const card = {
|
|
61
|
+
hidden: { opacity: 0, y: 12 },
|
|
62
|
+
visible: {
|
|
63
|
+
opacity: 1,
|
|
64
|
+
y: 0,
|
|
65
|
+
transition: { duration: 0.3, ease: [0.16, 1, 0.3, 1] },
|
|
66
|
+
},
|
|
67
|
+
};
|
|
59
68
|
|
|
60
|
-
|
|
61
|
-
<motion.div layout transition={{ type: 'spring', stiffness: 300 }} />
|
|
69
|
+
<motion.div variants={card} initial="hidden" animate="visible" />
|
|
62
70
|
```
|
|
63
71
|
|
|
72
|
+
Use variants for shared timing and maintainability.
|
|
73
|
+
|
|
64
74
|
## Exit Animations (AnimatePresence)
|
|
65
75
|
|
|
66
76
|
```tsx
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
<AnimatePresence>
|
|
70
|
-
{isVisible && (
|
|
77
|
+
<AnimatePresence mode="wait">
|
|
78
|
+
{open && (
|
|
71
79
|
<motion.div
|
|
72
|
-
key="
|
|
73
|
-
initial={{ opacity: 0 }}
|
|
74
|
-
animate={{ opacity: 1 }}
|
|
75
|
-
exit={{ opacity: 0 }}
|
|
80
|
+
key="panel"
|
|
81
|
+
initial={{ opacity: 0, y: 8 }}
|
|
82
|
+
animate={{ opacity: 1, y: 0 }}
|
|
83
|
+
exit={{ opacity: 0, y: 4 }}
|
|
84
|
+
transition={{ duration: 0.22, ease: [0.4, 0, 1, 1] }}
|
|
76
85
|
/>
|
|
77
86
|
)}
|
|
78
87
|
</AnimatePresence>
|
|
79
88
|
```
|
|
80
89
|
|
|
81
|
-
|
|
90
|
+
Always provide stable `key` values for exiting elements.
|
|
91
|
+
|
|
92
|
+
## Stagger Patterns
|
|
82
93
|
|
|
83
94
|
```tsx
|
|
84
95
|
const container = {
|
|
85
96
|
hidden: { opacity: 0 },
|
|
86
97
|
visible: {
|
|
87
98
|
opacity: 1,
|
|
88
|
-
transition: {
|
|
89
|
-
|
|
99
|
+
transition: {
|
|
100
|
+
staggerChildren: 0.05,
|
|
101
|
+
delayChildren: 0.05,
|
|
102
|
+
},
|
|
103
|
+
},
|
|
90
104
|
};
|
|
91
105
|
|
|
92
106
|
const item = {
|
|
93
|
-
hidden: { opacity: 0, y:
|
|
94
|
-
visible: { opacity: 1, y: 0 }
|
|
107
|
+
hidden: { opacity: 0, y: 8 },
|
|
108
|
+
visible: { opacity: 1, y: 0 },
|
|
95
109
|
};
|
|
96
|
-
|
|
97
|
-
<motion.ul variants={container} initial="hidden" animate="visible">
|
|
98
|
-
{items.map(i => <motion.li key={i} variants={item} />)}
|
|
99
|
-
</motion.ul>
|
|
100
110
|
```
|
|
101
111
|
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
```tsx
|
|
105
|
-
// Spring (default for physical properties)
|
|
106
|
-
transition={{ type: 'spring', stiffness: 300, damping: 20 }}
|
|
112
|
+
Cap total stagger windows to ~500ms.
|
|
107
113
|
|
|
108
|
-
|
|
109
|
-
transition={{ type: 'tween', duration: 0.5, ease: 'easeInOut' }}
|
|
114
|
+
## Layout Animations
|
|
110
115
|
|
|
111
|
-
|
|
112
|
-
|
|
116
|
+
```tsx
|
|
117
|
+
<motion.div layout />
|
|
113
118
|
```
|
|
114
119
|
|
|
115
|
-
|
|
120
|
+
Use `layout` for reordering and size changes. Add spring only when needed:
|
|
116
121
|
|
|
117
122
|
```tsx
|
|
118
|
-
|
|
119
|
-
ease: 'easeIn' | 'easeOut' | 'easeInOut'
|
|
120
|
-
ease: 'circIn' | 'circOut' | 'circInOut'
|
|
121
|
-
ease: 'backIn' | 'backOut' | 'backInOut'
|
|
122
|
-
ease: [0.4, 0, 0.2, 1] // cubic-bezier
|
|
123
|
+
<motion.div layout transition={{ type: 'spring', stiffness: 320, damping: 28 }} />
|
|
123
124
|
```
|
|
124
125
|
|
|
125
|
-
##
|
|
126
|
+
## Gestures
|
|
126
127
|
|
|
127
128
|
```tsx
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
const handleClick = async () => {
|
|
134
|
-
await animate(scope.current, { x: 100 });
|
|
135
|
-
await animate(scope.current, { scale: 1.2 });
|
|
136
|
-
};
|
|
137
|
-
|
|
138
|
-
return <div ref={scope} onClick={handleClick} />;
|
|
139
|
-
}
|
|
129
|
+
<motion.button
|
|
130
|
+
whileHover={{ scale: 1.02 }}
|
|
131
|
+
whileTap={{ scale: 0.98 }}
|
|
132
|
+
transition={{ duration: 0.12 }}
|
|
133
|
+
/>
|
|
140
134
|
```
|
|
141
135
|
|
|
142
|
-
|
|
136
|
+
Keep gesture amplitudes subtle (`0.98-1.03`).
|
|
143
137
|
|
|
144
|
-
|
|
145
|
-
import { useMotionValue, useTransform } from 'motion/react';
|
|
138
|
+
## Height Expand/Collapse (No height animation)
|
|
146
139
|
|
|
147
|
-
|
|
148
|
-
|
|
140
|
+
Use CSS grid technique:
|
|
141
|
+
|
|
142
|
+
```css
|
|
143
|
+
.accordion-content {
|
|
144
|
+
display: grid;
|
|
145
|
+
grid-template-rows: 0fr;
|
|
146
|
+
transition: grid-template-rows 280ms cubic-bezier(0.16, 1, 0.3, 1);
|
|
147
|
+
}
|
|
149
148
|
|
|
150
|
-
|
|
149
|
+
.accordion-content[data-open='true'] {
|
|
150
|
+
grid-template-rows: 1fr;
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
.accordion-inner {
|
|
154
|
+
overflow: hidden;
|
|
155
|
+
}
|
|
151
156
|
```
|
|
152
157
|
|
|
153
|
-
##
|
|
158
|
+
## Reduced Motion (Mandatory)
|
|
154
159
|
|
|
155
|
-
```
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
160
|
+
```css
|
|
161
|
+
@media (prefers-reduced-motion: reduce) {
|
|
162
|
+
* {
|
|
163
|
+
animation-duration: 0.01ms !important;
|
|
164
|
+
animation-iteration-count: 1 !important;
|
|
165
|
+
transition-duration: 0.01ms !important;
|
|
166
|
+
scroll-behavior: auto !important;
|
|
167
|
+
}
|
|
168
|
+
}
|
|
162
169
|
```
|
|
163
170
|
|
|
164
|
-
|
|
171
|
+
For motion/react, switch spatial movement to opacity-only when reduced motion is enabled.
|
|
172
|
+
|
|
173
|
+
## Quick Checklist
|
|
165
174
|
|
|
166
|
-
- [ ]
|
|
167
|
-
- [ ]
|
|
168
|
-
- [ ]
|
|
169
|
-
- [ ]
|
|
170
|
-
- [ ]
|
|
171
|
-
- [ ]
|
|
175
|
+
- [ ] Uses `motion/react` import
|
|
176
|
+
- [ ] Timing follows 100/300/500ms system
|
|
177
|
+
- [ ] Exponential easing, no bounce/elastic
|
|
178
|
+
- [ ] Animates only `transform` and `opacity`
|
|
179
|
+
- [ ] Uses `AnimatePresence` for exit states
|
|
180
|
+
- [ ] Includes reduced motion support
|
|
181
|
+
- [ ] Stagger windows stay under 500ms
|
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
# Color System — OKLCH Deep Guide
|
|
2
|
+
|
|
3
|
+
## Why OKLCH Over HSL
|
|
4
|
+
|
|
5
|
+
HSL is **not perceptually uniform** — `hsl(60, 100%, 50%)` (yellow) appears far brighter than `hsl(240, 100%, 50%)` (blue) at the same lightness. OKLCH fixes this: equal lightness steps _look_ equal.
|
|
6
|
+
|
|
7
|
+
```css
|
|
8
|
+
/* HSL: looks inconsistent */
|
|
9
|
+
--blue: hsl(240, 70%, 50%);
|
|
10
|
+
--green: hsl(120, 70%, 50%); /* Appears much brighter */
|
|
11
|
+
|
|
12
|
+
/* OKLCH: looks consistent */
|
|
13
|
+
--blue: oklch(0.55 0.22 264);
|
|
14
|
+
--green: oklch(0.55 0.18 145); /* Same perceived brightness */
|
|
15
|
+
```
|
|
16
|
+
|
|
17
|
+
## Two-Layer Token Architecture
|
|
18
|
+
|
|
19
|
+
**Layer 1 — Primitives** (raw values, never used directly in components):
|
|
20
|
+
|
|
21
|
+
```css
|
|
22
|
+
@theme {
|
|
23
|
+
--blue-100: oklch(0.95 0.03 264);
|
|
24
|
+
--blue-300: oklch(0.75 0.1 264);
|
|
25
|
+
--blue-500: oklch(0.55 0.22 264);
|
|
26
|
+
--blue-700: oklch(0.4 0.18 264);
|
|
27
|
+
--blue-900: oklch(0.25 0.12 264);
|
|
28
|
+
}
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
**Layer 2 — Semantic** (what components reference; redefine for dark mode):
|
|
32
|
+
|
|
33
|
+
```css
|
|
34
|
+
@theme {
|
|
35
|
+
--color-primary: var(--blue-500);
|
|
36
|
+
--color-primary-hover: var(--blue-700);
|
|
37
|
+
--color-surface: var(--neutral-50);
|
|
38
|
+
--color-surface-elevated: var(--neutral-0);
|
|
39
|
+
--color-text: var(--neutral-900);
|
|
40
|
+
--color-text-muted: var(--neutral-600);
|
|
41
|
+
--color-border: var(--neutral-200);
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/* Dark mode: redefine ONLY semantic layer */
|
|
45
|
+
@media (prefers-color-scheme: dark) {
|
|
46
|
+
@theme {
|
|
47
|
+
--color-primary: var(--blue-300);
|
|
48
|
+
--color-primary-hover: var(--blue-100);
|
|
49
|
+
--color-surface: var(--neutral-900);
|
|
50
|
+
--color-surface-elevated: var(--neutral-800);
|
|
51
|
+
--color-text: var(--neutral-100);
|
|
52
|
+
--color-text-muted: var(--neutral-400);
|
|
53
|
+
--color-border: var(--neutral-700);
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
```
|
|
57
|
+
|
|
58
|
+
## Tinted Neutrals
|
|
59
|
+
|
|
60
|
+
Never use pure gray (`chroma: 0`). Add `chroma: 0.01` hinted toward brand hue — barely visible but creates subconscious cohesion:
|
|
61
|
+
|
|
62
|
+
```css
|
|
63
|
+
@theme {
|
|
64
|
+
/* Brand hue: 264 (blue) — neutrals subtly tinted */
|
|
65
|
+
--neutral-0: oklch(1 0.005 264);
|
|
66
|
+
--neutral-50: oklch(0.97 0.01 264);
|
|
67
|
+
--neutral-100: oklch(0.93 0.01 264);
|
|
68
|
+
--neutral-200: oklch(0.87 0.01 264);
|
|
69
|
+
--neutral-400: oklch(0.7 0.01 264);
|
|
70
|
+
--neutral-600: oklch(0.5 0.01 264);
|
|
71
|
+
--neutral-800: oklch(0.25 0.01 264);
|
|
72
|
+
--neutral-900: oklch(0.16 0.01 264);
|
|
73
|
+
}
|
|
74
|
+
```
|
|
75
|
+
|
|
76
|
+
## Dark Mode Rules
|
|
77
|
+
|
|
78
|
+
Dark mode is **not** inverted light mode:
|
|
79
|
+
|
|
80
|
+
| Aspect | Light Mode | Dark Mode |
|
|
81
|
+
| ------- | ---------------------------- | ---------------------------------------------------- |
|
|
82
|
+
| Depth | Shadows create elevation | Lighter surfaces create elevation |
|
|
83
|
+
| Base | `oklch(0.97+ …)` | `oklch(0.15-0.18 …)` — NOT pure black |
|
|
84
|
+
| Accents | Full saturation | Desaturate 10-20% to reduce glare |
|
|
85
|
+
| Text | Dark on light, high contrast | Light on dark, slightly reduced contrast for comfort |
|
|
86
|
+
| Borders | Darker than surface | Lighter than surface |
|
|
87
|
+
|
|
88
|
+
```css
|
|
89
|
+
/* Dark surface elevation via lightness, not shadows */
|
|
90
|
+
--surface-0: oklch(0.15 0.01 264); /* Base */
|
|
91
|
+
--surface-1: oklch(0.18 0.01 264); /* Cards */
|
|
92
|
+
--surface-2: oklch(0.22 0.01 264); /* Modals */
|
|
93
|
+
--surface-3: oklch(0.26 0.01 264); /* Tooltips */
|
|
94
|
+
```
|
|
95
|
+
|
|
96
|
+
## 60-30-10 Rule
|
|
97
|
+
|
|
98
|
+
Applied to **visual weight**, not pixel count:
|
|
99
|
+
|
|
100
|
+
- 60% dominant (background, surface colors)
|
|
101
|
+
- 30% secondary (text, icons, subtle accents)
|
|
102
|
+
- 10% accent (CTAs, highlights, active states)
|
|
103
|
+
|
|
104
|
+
Accent works _because_ it's rare — overuse kills its power.
|
|
105
|
+
|
|
106
|
+
## Common Mistakes
|
|
107
|
+
|
|
108
|
+
- **Alpha as palette**: Heavy `rgba()` / transparency means incomplete palette — define explicit overlay colors per context
|
|
109
|
+
- **Gray text on colored background**: Use a darker shade of the background color or apply the text color at reduced opacity
|
|
110
|
+
- **Same accent everywhere**: If everything is "primary blue," nothing stands out
|
|
111
|
+
- **Ignoring contrast in dark mode**: WCAG 4.5:1 for body text, 3:1 for large text and UI components
|
|
@@ -0,0 +1,149 @@
|
|
|
1
|
+
# Interaction Design
|
|
2
|
+
|
|
3
|
+
## The 8 Interactive States
|
|
4
|
+
|
|
5
|
+
Every interactive element must design for ALL of these:
|
|
6
|
+
|
|
7
|
+
| State | When | Visual Treatment |
|
|
8
|
+
| -------- | --------------------- | --------------------------------------------- |
|
|
9
|
+
| Default | Resting | Base appearance |
|
|
10
|
+
| Hover | Cursor over (desktop) | Subtle shift — color, shadow, or translate |
|
|
11
|
+
| Focus | Keyboard navigation | `:focus-visible` ring (NOT `:focus`) |
|
|
12
|
+
| Active | Being pressed/clicked | Compressed/depressed feedback |
|
|
13
|
+
| Disabled | Not available | Reduced opacity (0.5) + `cursor: not-allowed` |
|
|
14
|
+
| Loading | Processing | Spinner or skeleton, disable interaction |
|
|
15
|
+
| Error | Validation failed | Red border + error message below |
|
|
16
|
+
| Success | Action completed | Green confirmation, brief |
|
|
17
|
+
|
|
18
|
+
## Focus Management
|
|
19
|
+
|
|
20
|
+
```css
|
|
21
|
+
/* Remove default focus for mouse, show for keyboard */
|
|
22
|
+
:focus:not(:focus-visible) {
|
|
23
|
+
outline: none;
|
|
24
|
+
}
|
|
25
|
+
:focus-visible {
|
|
26
|
+
outline: 2px solid var(--color-primary);
|
|
27
|
+
outline-offset: 2px;
|
|
28
|
+
}
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
**Focus ring rules:**
|
|
32
|
+
|
|
33
|
+
- 2-3px thick
|
|
34
|
+
- Offset from element edge
|
|
35
|
+
- 3:1 minimum contrast against adjacent colors
|
|
36
|
+
- Consistent style across ALL interactive elements
|
|
37
|
+
|
|
38
|
+
## Native Dialog + Inert
|
|
39
|
+
|
|
40
|
+
Use `<dialog>` with the `inert` attribute — eliminates complex focus-trapping JS:
|
|
41
|
+
|
|
42
|
+
```html
|
|
43
|
+
<dialog id="modal">
|
|
44
|
+
<form method="dialog">
|
|
45
|
+
<h2>Confirm action</h2>
|
|
46
|
+
<button value="cancel">Cancel</button>
|
|
47
|
+
<button value="confirm">Confirm</button>
|
|
48
|
+
</form>
|
|
49
|
+
</dialog>
|
|
50
|
+
|
|
51
|
+
<main id="content">…</main>
|
|
52
|
+
|
|
53
|
+
<script>
|
|
54
|
+
const modal = document.getElementById("modal");
|
|
55
|
+
modal.showModal();
|
|
56
|
+
document.getElementById("content").inert = true;
|
|
57
|
+
</script>
|
|
58
|
+
```
|
|
59
|
+
|
|
60
|
+
## Popover API
|
|
61
|
+
|
|
62
|
+
For tooltips, dropdowns, and popovers — light-dismiss, proper stacking, accessible by default:
|
|
63
|
+
|
|
64
|
+
```html
|
|
65
|
+
<button popovertarget="menu">Options</button>
|
|
66
|
+
<div id="menu" popover>
|
|
67
|
+
<button>Edit</button>
|
|
68
|
+
<button>Delete</button>
|
|
69
|
+
</div>
|
|
70
|
+
```
|
|
71
|
+
|
|
72
|
+
No z-index wars. No portal wrappers. Built-in light dismiss.
|
|
73
|
+
|
|
74
|
+
## Form Validation Timing
|
|
75
|
+
|
|
76
|
+
- **Validate on blur**, not on keystroke (exception: password strength meters)
|
|
77
|
+
- Show errors below the field, not in toast/alert
|
|
78
|
+
- Inline validation: red border + message appears when field loses focus and is invalid
|
|
79
|
+
- Clear error when user starts correcting
|
|
80
|
+
|
|
81
|
+
## Loading Patterns
|
|
82
|
+
|
|
83
|
+
| Pattern | Use When | Why |
|
|
84
|
+
| ---------------- | --------------------------------- | -------------------------------------- |
|
|
85
|
+
| Skeleton screens | Page/component loading | Previews content shape, feels faster |
|
|
86
|
+
| Inline spinner | Button action processing | Keeps context, shows progress |
|
|
87
|
+
| Progress bar | Known duration (upload, download) | Sets expectations |
|
|
88
|
+
| Optimistic UI | Low-stakes actions (like, toggle) | Update immediately, sync in background |
|
|
89
|
+
|
|
90
|
+
**Avoid**: Full-page spinners, blocking modals for non-destructive actions.
|
|
91
|
+
|
|
92
|
+
## Undo Over Confirmation
|
|
93
|
+
|
|
94
|
+
Users click through confirmation dialogs mindlessly. Prefer undo:
|
|
95
|
+
|
|
96
|
+
```
|
|
97
|
+
❌ "Are you sure you want to delete?" → [Cancel] [Delete]
|
|
98
|
+
✅ Item deleted. [Undo] (5 second window)
|
|
99
|
+
```
|
|
100
|
+
|
|
101
|
+
Exception: irreversible actions with severe consequences (account deletion, payment).
|
|
102
|
+
|
|
103
|
+
## Touch Target Expansion
|
|
104
|
+
|
|
105
|
+
Visual size can be small; tap target must be 44x44px minimum:
|
|
106
|
+
|
|
107
|
+
```css
|
|
108
|
+
.icon-button {
|
|
109
|
+
width: 24px;
|
|
110
|
+
height: 24px;
|
|
111
|
+
position: relative;
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
.icon-button::before {
|
|
115
|
+
content: "";
|
|
116
|
+
position: absolute;
|
|
117
|
+
inset: -10px; /* Expands tap area to 44x44 */
|
|
118
|
+
}
|
|
119
|
+
```
|
|
120
|
+
|
|
121
|
+
## Scroll Behavior
|
|
122
|
+
|
|
123
|
+
```css
|
|
124
|
+
html {
|
|
125
|
+
scroll-behavior: smooth;
|
|
126
|
+
scroll-padding-top: 80px; /* Account for sticky header */
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
@media (prefers-reduced-motion: reduce) {
|
|
130
|
+
html {
|
|
131
|
+
scroll-behavior: auto;
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
```
|
|
135
|
+
|
|
136
|
+
## Z-Index Scale
|
|
137
|
+
|
|
138
|
+
Named semantic layers prevent z-index wars:
|
|
139
|
+
|
|
140
|
+
```css
|
|
141
|
+
@theme {
|
|
142
|
+
--z-dropdown: 10;
|
|
143
|
+
--z-sticky: 20;
|
|
144
|
+
--z-modal-backdrop: 30;
|
|
145
|
+
--z-modal: 40;
|
|
146
|
+
--z-toast: 50;
|
|
147
|
+
--z-tooltip: 60;
|
|
148
|
+
}
|
|
149
|
+
```
|
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
# Typography Rules
|
|
2
|
+
|
|
3
|
+
## Core Principles
|
|
4
|
+
|
|
5
|
+
Typography carries hierarchy before color or decoration. Prioritize readability, rhythm, and intent.
|
|
6
|
+
|
|
7
|
+
- Use a single type scale ratio across the interface
|
|
8
|
+
- Keep body text highly readable before styling display text
|
|
9
|
+
- Limit font families: one display + one body (optional mono for code/data)
|
|
10
|
+
- Use weight and size for hierarchy before using color
|
|
11
|
+
|
|
12
|
+
## Fluid Type with clamp()
|
|
13
|
+
|
|
14
|
+
Use fluid sizing for responsive typography without breakpoint jumps:
|
|
15
|
+
|
|
16
|
+
```css
|
|
17
|
+
@theme {
|
|
18
|
+
--text-xs: clamp(0.75rem, 0.72rem + 0.15vw, 0.8125rem);
|
|
19
|
+
--text-sm: clamp(0.875rem, 0.84rem + 0.2vw, 0.9375rem);
|
|
20
|
+
--text-base: clamp(1rem, 0.95rem + 0.25vw, 1.125rem);
|
|
21
|
+
--text-lg: clamp(1.25rem, 1.15rem + 0.5vw, 1.5rem);
|
|
22
|
+
--text-xl: clamp(1.5rem, 1.35rem + 0.75vw, 2rem);
|
|
23
|
+
}
|
|
24
|
+
```
|
|
25
|
+
|
|
26
|
+
Never use fixed `px` for body text.
|
|
27
|
+
|
|
28
|
+
## Modular Scale
|
|
29
|
+
|
|
30
|
+
Pick one ratio and stick to it:
|
|
31
|
+
|
|
32
|
+
- **1.25** (major third) for practical UI
|
|
33
|
+
- **1.333** (perfect fourth) for editorial styles
|
|
34
|
+
|
|
35
|
+
Limit to 5-7 text sizes. Too many sizes destroys rhythm.
|
|
36
|
+
|
|
37
|
+
## Line Length and Rhythm
|
|
38
|
+
|
|
39
|
+
- Body measure: `max-width: 65ch`
|
|
40
|
+
- Comfortable line height: `1.45-1.7` for body text
|
|
41
|
+
- Tight headings: `1.05-1.25`
|
|
42
|
+
- Keep spacing tied to text rhythm (4pt system)
|
|
43
|
+
|
|
44
|
+
```css
|
|
45
|
+
.article {
|
|
46
|
+
font-size: var(--text-base);
|
|
47
|
+
line-height: 1.6;
|
|
48
|
+
max-width: 65ch;
|
|
49
|
+
}
|
|
50
|
+
```
|
|
51
|
+
|
|
52
|
+
## Font Selection
|
|
53
|
+
|
|
54
|
+
Avoid default AI fingerprints:
|
|
55
|
+
|
|
56
|
+
- Banned as primary display fonts: Inter, Roboto, Arial, Open Sans, Lato, Montserrat, Space Grotesk, system-ui
|
|
57
|
+
|
|
58
|
+
Preferred directions:
|
|
59
|
+
|
|
60
|
+
- Sans: Instrument Sans, Plus Jakarta Sans, Outfit, Onest, Figtree, Urbanist
|
|
61
|
+
- Editorial serif: Fraunces, Newsreader
|
|
62
|
+
|
|
63
|
+
## OpenType Features
|
|
64
|
+
|
|
65
|
+
Use OpenType intentionally:
|
|
66
|
+
|
|
67
|
+
```css
|
|
68
|
+
.data-table {
|
|
69
|
+
font-variant-numeric: tabular-nums;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
.recipe {
|
|
73
|
+
font-variant-numeric: diagonal-fractions;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
.abbrev {
|
|
77
|
+
font-variant-caps: all-small-caps;
|
|
78
|
+
letter-spacing: 0.04em;
|
|
79
|
+
}
|
|
80
|
+
```
|
|
81
|
+
|
|
82
|
+
## Dark Mode Typography
|
|
83
|
+
|
|
84
|
+
Light-on-dark text appears heavier. Adjust:
|
|
85
|
+
|
|
86
|
+
- Increase line height by `+0.05` to `+0.1`
|
|
87
|
+
- Reduce font weight if text feels too dense
|
|
88
|
+
- Avoid pure white text; use slightly tinted near-white
|
|
89
|
+
|
|
90
|
+
```css
|
|
91
|
+
@media (prefers-color-scheme: dark) {
|
|
92
|
+
body {
|
|
93
|
+
line-height: 1.65;
|
|
94
|
+
color: oklch(0.93 0.01 264);
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
```
|
|
98
|
+
|
|
99
|
+
## Quick Audit Checklist
|
|
100
|
+
|
|
101
|
+
- [ ] Body text uses `rem` or `em`, not `px`
|
|
102
|
+
- [ ] Type scale uses one ratio (1.25 or 1.333)
|
|
103
|
+
- [ ] Body lines capped around 65ch
|
|
104
|
+
- [ ] Headings and body have distinct line-height behavior
|
|
105
|
+
- [ ] OpenType features used for data/fractions/abbreviations where relevant
|
|
106
|
+
- [ ] Dark mode typography adjusted (not just color-inverted)
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
# UX Writing
|
|
2
|
+
|
|
3
|
+
## Button Labels
|
|
4
|
+
|
|
5
|
+
Specific verb + object. Never generic:
|
|
6
|
+
|
|
7
|
+
| Bad | Good | Why |
|
|
8
|
+
| ---------- | --------------- | ------------------------------ |
|
|
9
|
+
| OK | Save changes | Says what happens |
|
|
10
|
+
| Submit | Create account | Describes the outcome |
|
|
11
|
+
| Yes | Delete message | Confirms what gets deleted |
|
|
12
|
+
| Cancel | Keep editing | Tells user what "cancel" means |
|
|
13
|
+
| Click here | Download report | Verb + object, no "click" |
|
|
14
|
+
|
|
15
|
+
## Error Messages
|
|
16
|
+
|
|
17
|
+
Three-part formula — every time:
|
|
18
|
+
|
|
19
|
+
1. **What happened** (not "Error occurred")
|
|
20
|
+
2. **Why** (if known)
|
|
21
|
+
3. **How to fix**
|
|
22
|
+
|
|
23
|
+
```
|
|
24
|
+
|
|
25
|
+
Bad: "Error: Invalid input"
|
|
26
|
+
Bad: "Something went wrong"
|
|
27
|
+
Bad: "Oops! That didn't work"
|
|
28
|
+
|
|
29
|
+
Good: "Email address is invalid. Check for typos or use a different address."
|
|
30
|
+
Good: "Payment declined — your card issuer rejected the charge. Try a different card or contact your bank."
|
|
31
|
+
Good: "File too large (52 MB). Maximum size is 25 MB. Compress the file or split it into parts."
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
**Never use humor for errors.** Users are frustrated — be helpful, not cute.
|
|
35
|
+
|
|
36
|
+
## Empty States
|
|
37
|
+
|
|
38
|
+
Empty states are **onboarding opportunities**, not blank screens:
|
|
39
|
+
|
|
40
|
+
1. **Acknowledge**: "No messages yet"
|
|
41
|
+
2. **Explain value**: "Messages from your team will appear here"
|
|
42
|
+
3. **Clear action**: [Start a conversation]
|
|
43
|
+
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
Bad: (blank white space)
|
|
47
|
+
Bad: "No data"
|
|
48
|
+
Bad: "Nothing to show"
|
|
49
|
+
|
|
50
|
+
Good: "No projects yet — Create your first project to get started" [Create project]
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
## Confirmation vs Information
|
|
54
|
+
|
|
55
|
+
| Type | Pattern | Example |
|
|
56
|
+
| ----------- | ----------------------------------------- | -------------------------------------- |
|
|
57
|
+
| Success | Brief toast (auto-dismiss 3-5s) | "Changes saved" |
|
|
58
|
+
| Warning | Inline banner (persistent, dismissible) | "Your trial ends in 3 days" |
|
|
59
|
+
| Error | Inline at source (persistent until fixed) | "Password must be 8+ characters" |
|
|
60
|
+
| Destructive | Confirmation dialog (requires action) | "Delete account? This can't be undone" |
|
|
61
|
+
|
|
62
|
+
## Consistent Terminology
|
|
63
|
+
|
|
64
|
+
Pick ONE term and use it everywhere:
|
|
65
|
+
|
|
66
|
+
| Pick one | Not these |
|
|
67
|
+
| ---------------- | ----------------------------------- |
|
|
68
|
+
| Delete | Remove, Trash, Erase, Destroy |
|
|
69
|
+
| Settings | Preferences, Options, Configuration |
|
|
70
|
+
| Log in / Log out | Sign in / Sign out (pick a pair) |
|
|
71
|
+
| Save | Submit, Apply, Update, Confirm |
|
|
72
|
+
|
|
73
|
+
## Voice vs Tone
|
|
74
|
+
|
|
75
|
+
- **Voice** = consistent brand personality (always the same)
|
|
76
|
+
- **Tone** = adapts to moment:
|
|
77
|
+
- Success → celebratory, brief
|
|
78
|
+
- Error → empathetic, helpful
|
|
79
|
+
- Onboarding → encouraging, clear
|
|
80
|
+
- Warning → direct, specific
|
|
81
|
+
|
|
82
|
+
## Internationalization
|
|
83
|
+
|
|
84
|
+
- **Plan for 30% text expansion** — German, French, Finnish expand significantly
|
|
85
|
+
- Design layouts with flexible text containers
|
|
86
|
+
- Never truncate translated text without tooltip/expansion
|
|
87
|
+
- Use ICU message format for plurals: `{count, plural, one {# item} other {# items}}`
|
|
88
|
+
- Right-to-left (RTL) support: use logical properties (`margin-inline-start` not `margin-left`)
|
|
89
|
+
|
|
90
|
+
## Microcopy Checklist
|
|
91
|
+
|
|
92
|
+
- [ ] All buttons use verb + object
|
|
93
|
+
- [ ] All errors follow 3-part formula
|
|
94
|
+
- [ ] All empty states have acknowledgment + value + action
|
|
95
|
+
- [ ] Terminology is consistent across all screens
|
|
96
|
+
- [ ] No humor in error states
|
|
97
|
+
- [ ] No "click here" or "click below"
|
|
98
|
+
- [ ] No ALL CAPS for body text (small-caps for abbreviations only)
|
|
99
|
+
- [ ] Loading states have context ("Loading messages..." not "Loading...")
|
package/package.json
CHANGED
|
File without changes
|