opencodekit 0.18.8 → 0.18.10
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/agent/explore.md +26 -9
- package/dist/template/.opencode/agent/general.md +3 -1
- package/dist/template/.opencode/agent/plan.md +4 -2
- package/dist/template/.opencode/agent/review.md +3 -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/dist/template/.opencode/skill/tilth-cli/SKILL.md +180 -0
- package/package.json +1 -1
- package/dist/template/.opencode/memory/memory.db +0 -0
|
@@ -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)
|