@dianzhong/create-harness-app 0.1.2 → 0.1.4
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.mjs +4 -0
- package/package.json +1 -1
- package/templates/harness/full/.claude/agents/code-reviewer.md +1 -1
- package/templates/harness/full/.claude/agents/harness-reviewer.md +0 -2
- package/templates/harness/full/.claude/rules/skills-mcp.md +2 -3
- package/templates/harness/full/.claude/settings.json +1 -1
- package/templates/harness/full/docs/ai-harness.md +4 -6
- package/templates/harness/full/docs/harness-quick-reference.md +1 -1
- package/templates/harness/full/docs/review-checklist.md +1 -1
- package/templates/harness/full/scripts/verify-skills.mjs +6 -61
- package/templates/harness/full/scripts/verify-skills.test.mjs +1 -11
- package/templates/harness/full/.agents/skills/find-skills/SKILL.md +0 -143
- package/templates/harness/full/.agents/skills/vue-best-practices/LICENSE.md +0 -21
- package/templates/harness/full/.agents/skills/vue-best-practices/SKILL.md +0 -155
- package/templates/harness/full/.agents/skills/vue-best-practices/SYNC.md +0 -5
- package/templates/harness/full/.agents/skills/vue-best-practices/references/animation-class-based-technique.md +0 -258
- package/templates/harness/full/.agents/skills/vue-best-practices/references/animation-state-driven-technique.md +0 -287
- package/templates/harness/full/.agents/skills/vue-best-practices/references/component-async.md +0 -99
- package/templates/harness/full/.agents/skills/vue-best-practices/references/component-data-flow.md +0 -313
- package/templates/harness/full/.agents/skills/vue-best-practices/references/component-fallthrough-attrs.md +0 -179
- package/templates/harness/full/.agents/skills/vue-best-practices/references/component-keep-alive.md +0 -139
- package/templates/harness/full/.agents/skills/vue-best-practices/references/component-slots.md +0 -226
- package/templates/harness/full/.agents/skills/vue-best-practices/references/component-suspense.md +0 -231
- package/templates/harness/full/.agents/skills/vue-best-practices/references/component-teleport.md +0 -110
- package/templates/harness/full/.agents/skills/vue-best-practices/references/component-transition-group.md +0 -131
- package/templates/harness/full/.agents/skills/vue-best-practices/references/component-transition.md +0 -135
- package/templates/harness/full/.agents/skills/vue-best-practices/references/composables.md +0 -303
- package/templates/harness/full/.agents/skills/vue-best-practices/references/directives.md +0 -168
- package/templates/harness/full/.agents/skills/vue-best-practices/references/perf-avoid-component-abstraction-in-lists.md +0 -177
- package/templates/harness/full/.agents/skills/vue-best-practices/references/perf-v-once-v-memo-directives.md +0 -185
- package/templates/harness/full/.agents/skills/vue-best-practices/references/perf-virtualize-large-lists.md +0 -182
- package/templates/harness/full/.agents/skills/vue-best-practices/references/plugins.md +0 -178
- package/templates/harness/full/.agents/skills/vue-best-practices/references/reactivity.md +0 -371
- package/templates/harness/full/.agents/skills/vue-best-practices/references/render-functions.md +0 -227
- package/templates/harness/full/.agents/skills/vue-best-practices/references/sfc.md +0 -355
- package/templates/harness/full/.agents/skills/vue-best-practices/references/state-management.md +0 -138
- package/templates/harness/full/.agents/skills/vue-best-practices/references/updated-hook-performance.md +0 -193
- package/templates/harness/full/AGENTS.md +0 -3
- package/templates/harness/full/GEMINI.md +0 -3
|
@@ -1,258 +0,0 @@
|
|
|
1
|
-
---
|
|
2
|
-
title: Use Class-based Animations for Non-Enter/Leave Effects
|
|
3
|
-
impact: LOW
|
|
4
|
-
impactDescription: Class-based animations are simpler and more performant for elements that remain in the DOM
|
|
5
|
-
type: best-practice
|
|
6
|
-
tags: [vue3, animation, css, class-binding, state]
|
|
7
|
-
---
|
|
8
|
-
|
|
9
|
-
# Use Class-based Animations for Non-Enter/Leave Effects
|
|
10
|
-
|
|
11
|
-
**Impact: LOW** - For animations on elements that are not entering or leaving the DOM, use CSS class-based animations triggered by Vue's reactive state. This is simpler than `<Transition>` and more appropriate for feedback animations like shake, pulse, or highlight effects.
|
|
12
|
-
|
|
13
|
-
## Task List
|
|
14
|
-
|
|
15
|
-
- Use class-based animations for elements staying in the DOM
|
|
16
|
-
- Use `<Transition>` only for enter/leave animations
|
|
17
|
-
- Combine CSS animations with Vue's class bindings (`:class`)
|
|
18
|
-
- Consider using `setTimeout` to auto-remove animation classes
|
|
19
|
-
|
|
20
|
-
**When to Use Class-based Animations:**
|
|
21
|
-
|
|
22
|
-
- User feedback (shake on error, pulse on success)
|
|
23
|
-
- Attention-grabbing effects (highlight changes)
|
|
24
|
-
- Hover/focus states that need more than CSS transitions
|
|
25
|
-
- Any animation where the element stays mounted
|
|
26
|
-
|
|
27
|
-
**When to Use Transition Component:**
|
|
28
|
-
|
|
29
|
-
- Elements entering/leaving the DOM (v-if/v-show)
|
|
30
|
-
- Route transitions
|
|
31
|
-
- List item additions/removals
|
|
32
|
-
|
|
33
|
-
## Basic Pattern
|
|
34
|
-
|
|
35
|
-
```vue
|
|
36
|
-
<script setup>
|
|
37
|
-
import { ref } from 'vue'
|
|
38
|
-
|
|
39
|
-
const showError = ref(false)
|
|
40
|
-
|
|
41
|
-
function submitForm() {
|
|
42
|
-
if (!isValid()) {
|
|
43
|
-
// Trigger shake animation
|
|
44
|
-
showError.value = true
|
|
45
|
-
|
|
46
|
-
// Auto-remove class after animation completes
|
|
47
|
-
setTimeout(() => {
|
|
48
|
-
showError.value = false
|
|
49
|
-
}, 820) // Match animation duration
|
|
50
|
-
}
|
|
51
|
-
}
|
|
52
|
-
</script>
|
|
53
|
-
|
|
54
|
-
<template>
|
|
55
|
-
<div :class="{ shake: showError }">
|
|
56
|
-
<button @click="submitForm">Submit</button>
|
|
57
|
-
<span v-if="showError">This feature is disabled!</span>
|
|
58
|
-
</div>
|
|
59
|
-
</template>
|
|
60
|
-
|
|
61
|
-
<style>
|
|
62
|
-
.shake {
|
|
63
|
-
animation: shake 0.82s cubic-bezier(0.36, 0.07, 0.19, 0.97) both;
|
|
64
|
-
transform: translate3d(0, 0, 0); /* Enable GPU acceleration */
|
|
65
|
-
}
|
|
66
|
-
|
|
67
|
-
@keyframes shake {
|
|
68
|
-
10%,
|
|
69
|
-
90% {
|
|
70
|
-
transform: translate3d(-1px, 0, 0);
|
|
71
|
-
}
|
|
72
|
-
20%,
|
|
73
|
-
80% {
|
|
74
|
-
transform: translate3d(2px, 0, 0);
|
|
75
|
-
}
|
|
76
|
-
30%,
|
|
77
|
-
50%,
|
|
78
|
-
70% {
|
|
79
|
-
transform: translate3d(-4px, 0, 0);
|
|
80
|
-
}
|
|
81
|
-
40%,
|
|
82
|
-
60% {
|
|
83
|
-
transform: translate3d(4px, 0, 0);
|
|
84
|
-
}
|
|
85
|
-
}
|
|
86
|
-
</style>
|
|
87
|
-
```
|
|
88
|
-
|
|
89
|
-
## Common Animation Patterns
|
|
90
|
-
|
|
91
|
-
### Pulse on Success
|
|
92
|
-
|
|
93
|
-
```vue
|
|
94
|
-
<script setup>
|
|
95
|
-
import { ref } from 'vue'
|
|
96
|
-
|
|
97
|
-
const saved = ref(false)
|
|
98
|
-
|
|
99
|
-
async function save() {
|
|
100
|
-
await saveData()
|
|
101
|
-
saved.value = true
|
|
102
|
-
setTimeout(() => (saved.value = false), 1000)
|
|
103
|
-
}
|
|
104
|
-
</script>
|
|
105
|
-
|
|
106
|
-
<template>
|
|
107
|
-
<button :class="{ pulse: saved }" @click="save">
|
|
108
|
-
{{ saved ? 'Saved!' : 'Save' }}
|
|
109
|
-
</button>
|
|
110
|
-
</template>
|
|
111
|
-
|
|
112
|
-
<style>
|
|
113
|
-
.pulse {
|
|
114
|
-
animation: pulse 0.5s ease-in-out;
|
|
115
|
-
}
|
|
116
|
-
|
|
117
|
-
@keyframes pulse {
|
|
118
|
-
0%,
|
|
119
|
-
100% {
|
|
120
|
-
transform: scale(1);
|
|
121
|
-
}
|
|
122
|
-
50% {
|
|
123
|
-
transform: scale(1.05);
|
|
124
|
-
}
|
|
125
|
-
}
|
|
126
|
-
</style>
|
|
127
|
-
```
|
|
128
|
-
|
|
129
|
-
### Highlight on Change
|
|
130
|
-
|
|
131
|
-
```vue
|
|
132
|
-
<script setup>
|
|
133
|
-
import { ref, watch } from 'vue'
|
|
134
|
-
|
|
135
|
-
const value = ref(0)
|
|
136
|
-
const justUpdated = ref(false)
|
|
137
|
-
|
|
138
|
-
watch(value, () => {
|
|
139
|
-
justUpdated.value = true
|
|
140
|
-
setTimeout(() => (justUpdated.value = false), 1000)
|
|
141
|
-
})
|
|
142
|
-
</script>
|
|
143
|
-
|
|
144
|
-
<template>
|
|
145
|
-
<div :class="{ highlight: justUpdated }">Value: {{ value }}</div>
|
|
146
|
-
</template>
|
|
147
|
-
|
|
148
|
-
<style>
|
|
149
|
-
.highlight {
|
|
150
|
-
animation: highlight 1s ease-out;
|
|
151
|
-
}
|
|
152
|
-
|
|
153
|
-
@keyframes highlight {
|
|
154
|
-
0% {
|
|
155
|
-
background-color: yellow;
|
|
156
|
-
}
|
|
157
|
-
100% {
|
|
158
|
-
background-color: transparent;
|
|
159
|
-
}
|
|
160
|
-
}
|
|
161
|
-
</style>
|
|
162
|
-
```
|
|
163
|
-
|
|
164
|
-
### Bounce Attention
|
|
165
|
-
|
|
166
|
-
```vue
|
|
167
|
-
<script setup>
|
|
168
|
-
import { ref } from 'vue'
|
|
169
|
-
|
|
170
|
-
const needsAttention = ref(false)
|
|
171
|
-
|
|
172
|
-
function notifyUser() {
|
|
173
|
-
needsAttention.value = true
|
|
174
|
-
// No setTimeout needed - using animationend event
|
|
175
|
-
}
|
|
176
|
-
</script>
|
|
177
|
-
|
|
178
|
-
<template>
|
|
179
|
-
<div :class="{ bounce: needsAttention }" @animationend="needsAttention = false">
|
|
180
|
-
<BellIcon />
|
|
181
|
-
</div>
|
|
182
|
-
</template>
|
|
183
|
-
|
|
184
|
-
<style>
|
|
185
|
-
.bounce {
|
|
186
|
-
animation: bounce 0.5s ease;
|
|
187
|
-
}
|
|
188
|
-
|
|
189
|
-
@keyframes bounce {
|
|
190
|
-
0%,
|
|
191
|
-
100% {
|
|
192
|
-
transform: translateY(0);
|
|
193
|
-
}
|
|
194
|
-
50% {
|
|
195
|
-
transform: translateY(-10px);
|
|
196
|
-
}
|
|
197
|
-
}
|
|
198
|
-
</style>
|
|
199
|
-
```
|
|
200
|
-
|
|
201
|
-
## Using animationend Event
|
|
202
|
-
|
|
203
|
-
Instead of `setTimeout`, use the `animationend` event for cleaner code:
|
|
204
|
-
|
|
205
|
-
```vue
|
|
206
|
-
<script setup>
|
|
207
|
-
import { ref } from 'vue'
|
|
208
|
-
|
|
209
|
-
const isAnimating = ref(false)
|
|
210
|
-
|
|
211
|
-
function triggerAnimation() {
|
|
212
|
-
isAnimating.value = true
|
|
213
|
-
// Class is automatically removed when animation ends
|
|
214
|
-
}
|
|
215
|
-
</script>
|
|
216
|
-
|
|
217
|
-
<template>
|
|
218
|
-
<div :class="{ animate: isAnimating }" @animationend="isAnimating = false">Content</div>
|
|
219
|
-
</template>
|
|
220
|
-
```
|
|
221
|
-
|
|
222
|
-
## Composable for Reusable Animations
|
|
223
|
-
|
|
224
|
-
```javascript
|
|
225
|
-
// composables/useAnimation.js
|
|
226
|
-
import { ref } from 'vue'
|
|
227
|
-
|
|
228
|
-
export function useAnimation(duration = 500) {
|
|
229
|
-
const isAnimating = ref(false)
|
|
230
|
-
|
|
231
|
-
function trigger() {
|
|
232
|
-
isAnimating.value = true
|
|
233
|
-
setTimeout(() => {
|
|
234
|
-
isAnimating.value = false
|
|
235
|
-
}, duration)
|
|
236
|
-
}
|
|
237
|
-
|
|
238
|
-
return {
|
|
239
|
-
isAnimating,
|
|
240
|
-
trigger,
|
|
241
|
-
}
|
|
242
|
-
}
|
|
243
|
-
```
|
|
244
|
-
|
|
245
|
-
```vue
|
|
246
|
-
<script setup>
|
|
247
|
-
import { useAnimation } from '@/composables/useAnimation'
|
|
248
|
-
|
|
249
|
-
const shake = useAnimation(820)
|
|
250
|
-
const pulse = useAnimation(500)
|
|
251
|
-
</script>
|
|
252
|
-
|
|
253
|
-
<template>
|
|
254
|
-
<button :class="{ shake: shake.isAnimating.value }" @click="shake.trigger()">Shake me</button>
|
|
255
|
-
|
|
256
|
-
<button :class="{ pulse: pulse.isAnimating.value }" @click="pulse.trigger()">Pulse me</button>
|
|
257
|
-
</template>
|
|
258
|
-
```
|
|
@@ -1,287 +0,0 @@
|
|
|
1
|
-
---
|
|
2
|
-
title: State-driven Animations with CSS Transitions and Style Bindings
|
|
3
|
-
impact: LOW
|
|
4
|
-
impactDescription: Combining Vue's reactive style bindings with CSS transitions creates smooth, interactive animations
|
|
5
|
-
type: best-practice
|
|
6
|
-
tags: [vue3, animation, css, transition, style-binding, state, interactive]
|
|
7
|
-
---
|
|
8
|
-
|
|
9
|
-
# State-driven Animations with CSS Transitions and Style Bindings
|
|
10
|
-
|
|
11
|
-
**Impact: LOW** - For responsive, interactive animations that react to user input or state changes, combine Vue's dynamic style bindings with CSS transitions. This creates smooth animations that interpolate values in real-time based on state.
|
|
12
|
-
|
|
13
|
-
## Task List
|
|
14
|
-
|
|
15
|
-
- Use `:style` binding for dynamic properties that change frequently
|
|
16
|
-
- Add CSS `transition` property to smoothly animate between values
|
|
17
|
-
- Consider using `transform` and `opacity` for GPU-accelerated animations
|
|
18
|
-
- For complex value interpolation, use watchers with animation libraries
|
|
19
|
-
|
|
20
|
-
## Basic Pattern
|
|
21
|
-
|
|
22
|
-
```vue
|
|
23
|
-
<script setup>
|
|
24
|
-
import { ref } from 'vue'
|
|
25
|
-
|
|
26
|
-
const hue = ref(0)
|
|
27
|
-
|
|
28
|
-
function onMousemove(e) {
|
|
29
|
-
// Map mouse X position to hue (0-360)
|
|
30
|
-
const rect = e.currentTarget.getBoundingClientRect()
|
|
31
|
-
hue.value = Math.round(((e.clientX - rect.left) / rect.width) * 360)
|
|
32
|
-
}
|
|
33
|
-
</script>
|
|
34
|
-
|
|
35
|
-
<template>
|
|
36
|
-
<div
|
|
37
|
-
:style="{ backgroundColor: `hsl(${hue}, 80%, 50%)` }"
|
|
38
|
-
class="interactive-area"
|
|
39
|
-
@mousemove="onMousemove"
|
|
40
|
-
>
|
|
41
|
-
<p>Move your mouse across this div...</p>
|
|
42
|
-
<p>Hue: {{ hue }}</p>
|
|
43
|
-
</div>
|
|
44
|
-
</template>
|
|
45
|
-
|
|
46
|
-
<style>
|
|
47
|
-
.interactive-area {
|
|
48
|
-
transition: background-color 0.3s ease;
|
|
49
|
-
height: 200px;
|
|
50
|
-
display: flex;
|
|
51
|
-
flex-direction: column;
|
|
52
|
-
align-items: center;
|
|
53
|
-
justify-content: center;
|
|
54
|
-
}
|
|
55
|
-
</style>
|
|
56
|
-
```
|
|
57
|
-
|
|
58
|
-
## Common Use Cases
|
|
59
|
-
|
|
60
|
-
### Following Mouse Position
|
|
61
|
-
|
|
62
|
-
```vue
|
|
63
|
-
<script setup>
|
|
64
|
-
import { ref } from 'vue'
|
|
65
|
-
|
|
66
|
-
const x = ref(0)
|
|
67
|
-
const y = ref(0)
|
|
68
|
-
|
|
69
|
-
function onMousemove(e) {
|
|
70
|
-
const rect = e.currentTarget.getBoundingClientRect()
|
|
71
|
-
x.value = e.clientX - rect.left
|
|
72
|
-
y.value = e.clientY - rect.top
|
|
73
|
-
}
|
|
74
|
-
</script>
|
|
75
|
-
|
|
76
|
-
<template>
|
|
77
|
-
<div class="container" @mousemove="onMousemove">
|
|
78
|
-
<div
|
|
79
|
-
class="follower"
|
|
80
|
-
:style="{
|
|
81
|
-
transform: `translate(${x}px, ${y}px)`,
|
|
82
|
-
}"
|
|
83
|
-
/>
|
|
84
|
-
</div>
|
|
85
|
-
</template>
|
|
86
|
-
|
|
87
|
-
<style>
|
|
88
|
-
.container {
|
|
89
|
-
position: relative;
|
|
90
|
-
height: 300px;
|
|
91
|
-
}
|
|
92
|
-
|
|
93
|
-
.follower {
|
|
94
|
-
position: absolute;
|
|
95
|
-
width: 20px;
|
|
96
|
-
height: 20px;
|
|
97
|
-
background: blue;
|
|
98
|
-
border-radius: 50%;
|
|
99
|
-
/* Smooth following with transition */
|
|
100
|
-
transition: transform 0.1s ease-out;
|
|
101
|
-
/* Prevent the follower from triggering mousemove */
|
|
102
|
-
pointer-events: none;
|
|
103
|
-
}
|
|
104
|
-
</style>
|
|
105
|
-
```
|
|
106
|
-
|
|
107
|
-
### Progress Animation
|
|
108
|
-
|
|
109
|
-
```vue
|
|
110
|
-
<script setup>
|
|
111
|
-
import { ref } from 'vue'
|
|
112
|
-
|
|
113
|
-
const progress = ref(0)
|
|
114
|
-
</script>
|
|
115
|
-
|
|
116
|
-
<template>
|
|
117
|
-
<div class="progress-container">
|
|
118
|
-
<div class="progress-bar" :style="{ width: `${progress}%` }" />
|
|
119
|
-
</div>
|
|
120
|
-
<input v-model.number="progress" type="range" min="0" max="100" />
|
|
121
|
-
</template>
|
|
122
|
-
|
|
123
|
-
<style>
|
|
124
|
-
.progress-container {
|
|
125
|
-
height: 20px;
|
|
126
|
-
background: #e0e0e0;
|
|
127
|
-
border-radius: 10px;
|
|
128
|
-
overflow: hidden;
|
|
129
|
-
}
|
|
130
|
-
|
|
131
|
-
.progress-bar {
|
|
132
|
-
height: 100%;
|
|
133
|
-
background: linear-gradient(90deg, #4caf50, #8bc34a);
|
|
134
|
-
transition: width 0.3s ease;
|
|
135
|
-
}
|
|
136
|
-
</style>
|
|
137
|
-
```
|
|
138
|
-
|
|
139
|
-
### Scroll-based Animation
|
|
140
|
-
|
|
141
|
-
```vue
|
|
142
|
-
<script setup>
|
|
143
|
-
import { computed, onMounted, onUnmounted, ref } from 'vue'
|
|
144
|
-
|
|
145
|
-
const scrollY = ref(0)
|
|
146
|
-
|
|
147
|
-
const heroOpacity = computed(() => {
|
|
148
|
-
return Math.max(0, 1 - scrollY.value / 300)
|
|
149
|
-
})
|
|
150
|
-
|
|
151
|
-
const scrollOffset = computed(() => {
|
|
152
|
-
return scrollY.value * 0.5 // Parallax effect
|
|
153
|
-
})
|
|
154
|
-
|
|
155
|
-
function handleScroll() {
|
|
156
|
-
scrollY.value = window.scrollY
|
|
157
|
-
}
|
|
158
|
-
|
|
159
|
-
onMounted(() => {
|
|
160
|
-
window.addEventListener('scroll', handleScroll, { passive: true })
|
|
161
|
-
})
|
|
162
|
-
|
|
163
|
-
onUnmounted(() => {
|
|
164
|
-
window.removeEventListener('scroll', handleScroll)
|
|
165
|
-
})
|
|
166
|
-
</script>
|
|
167
|
-
|
|
168
|
-
<template>
|
|
169
|
-
<div
|
|
170
|
-
class="hero"
|
|
171
|
-
:style="{
|
|
172
|
-
opacity: heroOpacity,
|
|
173
|
-
transform: `translateY(${scrollOffset}px)`,
|
|
174
|
-
}"
|
|
175
|
-
>
|
|
176
|
-
<h1>Scroll Down</h1>
|
|
177
|
-
</div>
|
|
178
|
-
</template>
|
|
179
|
-
|
|
180
|
-
<style>
|
|
181
|
-
.hero {
|
|
182
|
-
height: 100vh;
|
|
183
|
-
display: flex;
|
|
184
|
-
align-items: center;
|
|
185
|
-
justify-content: center;
|
|
186
|
-
/* Note: No transition for scroll-based animations - they should be instant */
|
|
187
|
-
}
|
|
188
|
-
</style>
|
|
189
|
-
```
|
|
190
|
-
|
|
191
|
-
### Color Theme Transition
|
|
192
|
-
|
|
193
|
-
```vue
|
|
194
|
-
<script setup>
|
|
195
|
-
import { computed, ref } from 'vue'
|
|
196
|
-
|
|
197
|
-
const isDark = ref(false)
|
|
198
|
-
|
|
199
|
-
const themeStyles = computed(() => ({
|
|
200
|
-
'--bg-color': isDark.value ? '#1a1a1a' : '#ffffff',
|
|
201
|
-
'--text-color': isDark.value ? '#ffffff' : '#1a1a1a',
|
|
202
|
-
backgroundColor: 'var(--bg-color)',
|
|
203
|
-
color: 'var(--text-color)',
|
|
204
|
-
}))
|
|
205
|
-
|
|
206
|
-
function toggleTheme() {
|
|
207
|
-
isDark.value = !isDark.value
|
|
208
|
-
}
|
|
209
|
-
</script>
|
|
210
|
-
|
|
211
|
-
<template>
|
|
212
|
-
<div class="app" :style="themeStyles">
|
|
213
|
-
<button @click="toggleTheme">Toggle Theme</button>
|
|
214
|
-
<p>Current theme: {{ isDark ? 'Dark' : 'Light' }}</p>
|
|
215
|
-
</div>
|
|
216
|
-
</template>
|
|
217
|
-
|
|
218
|
-
<style>
|
|
219
|
-
.app {
|
|
220
|
-
min-height: 100vh;
|
|
221
|
-
transition:
|
|
222
|
-
background-color 0.5s ease,
|
|
223
|
-
color 0.5s ease;
|
|
224
|
-
}
|
|
225
|
-
</style>
|
|
226
|
-
```
|
|
227
|
-
|
|
228
|
-
## Advanced: Numerical Tweening with Watchers
|
|
229
|
-
|
|
230
|
-
For smooth number animations (counters, stats), use watchers with animation libraries:
|
|
231
|
-
|
|
232
|
-
```vue
|
|
233
|
-
<script setup>
|
|
234
|
-
import gsap from 'gsap'
|
|
235
|
-
|
|
236
|
-
import { computed, reactive, ref, watch } from 'vue'
|
|
237
|
-
|
|
238
|
-
const targetNumber = ref(0)
|
|
239
|
-
const tweened = reactive({ value: 0 })
|
|
240
|
-
|
|
241
|
-
// Computed for display
|
|
242
|
-
const displayNumber = computed(() => tweened.value)
|
|
243
|
-
|
|
244
|
-
watch(targetNumber, (newValue) => {
|
|
245
|
-
gsap.to(tweened, {
|
|
246
|
-
duration: 0.5,
|
|
247
|
-
value: Number(newValue) || 0,
|
|
248
|
-
ease: 'power2.out',
|
|
249
|
-
})
|
|
250
|
-
})
|
|
251
|
-
</script>
|
|
252
|
-
|
|
253
|
-
<template>
|
|
254
|
-
<div>
|
|
255
|
-
<input v-model.number="targetNumber" type="number" />
|
|
256
|
-
<p class="counter">
|
|
257
|
-
{{ displayNumber.toFixed(0) }}
|
|
258
|
-
</p>
|
|
259
|
-
</div>
|
|
260
|
-
</template>
|
|
261
|
-
```
|
|
262
|
-
|
|
263
|
-
## Performance Considerations
|
|
264
|
-
|
|
265
|
-
```vue
|
|
266
|
-
<style>
|
|
267
|
-
/* GOOD: GPU-accelerated properties */
|
|
268
|
-
.element {
|
|
269
|
-
transition:
|
|
270
|
-
transform 0.3s ease,
|
|
271
|
-
opacity 0.3s ease;
|
|
272
|
-
}
|
|
273
|
-
|
|
274
|
-
/* AVOID: Properties that trigger layout recalculation */
|
|
275
|
-
.element {
|
|
276
|
-
transition:
|
|
277
|
-
width 0.3s ease,
|
|
278
|
-
height 0.3s ease,
|
|
279
|
-
margin 0.3s ease;
|
|
280
|
-
}
|
|
281
|
-
|
|
282
|
-
/* For high-frequency updates, consider will-change */
|
|
283
|
-
.frequently-animated {
|
|
284
|
-
will-change: transform;
|
|
285
|
-
}
|
|
286
|
-
</style>
|
|
287
|
-
```
|
package/templates/harness/full/.agents/skills/vue-best-practices/references/component-async.md
DELETED
|
@@ -1,99 +0,0 @@
|
|
|
1
|
-
---
|
|
2
|
-
title: Async Component Best Practices
|
|
3
|
-
impact: MEDIUM
|
|
4
|
-
impactDescription: Poor async component strategy can delay interactivity in SSR apps and create loading UI flicker
|
|
5
|
-
type: best-practice
|
|
6
|
-
tags: [vue3, async-components, ssr, hydration, performance, ux]
|
|
7
|
-
---
|
|
8
|
-
|
|
9
|
-
# Async Component Best Practices
|
|
10
|
-
|
|
11
|
-
**Impact: MEDIUM** - Async components should reduce JavaScript cost without degrading perceived performance. Focus on hydration timing in SSR and stable loading UX.
|
|
12
|
-
|
|
13
|
-
## Task List
|
|
14
|
-
|
|
15
|
-
- Use lazy hydration strategies for non-critical SSR component trees
|
|
16
|
-
- Import only the hydration helpers you actually use
|
|
17
|
-
- Keep `loadingComponent` delay near the default `200ms` unless real UX data suggests otherwise
|
|
18
|
-
- Configure `delay` and `timeout` together for predictable loading behavior
|
|
19
|
-
|
|
20
|
-
## Use Lazy Hydration Strategies in SSR
|
|
21
|
-
|
|
22
|
-
In Vue 3.5+, async components can delay hydration until idle time, visibility, media query match, or user interaction.
|
|
23
|
-
|
|
24
|
-
**BAD:**
|
|
25
|
-
|
|
26
|
-
```vue
|
|
27
|
-
<script setup lang="ts">
|
|
28
|
-
import { defineAsyncComponent } from 'vue'
|
|
29
|
-
|
|
30
|
-
const AsyncComments = defineAsyncComponent({
|
|
31
|
-
loader: () => import('./Comments.vue'),
|
|
32
|
-
})
|
|
33
|
-
</script>
|
|
34
|
-
```
|
|
35
|
-
|
|
36
|
-
**GOOD:**
|
|
37
|
-
|
|
38
|
-
```vue
|
|
39
|
-
<script setup lang="ts">
|
|
40
|
-
import { defineAsyncComponent, hydrateOnIdle, hydrateOnVisible } from 'vue'
|
|
41
|
-
|
|
42
|
-
const AsyncComments = defineAsyncComponent({
|
|
43
|
-
loader: () => import('./Comments.vue'),
|
|
44
|
-
hydrate: hydrateOnVisible({ rootMargin: '100px' }),
|
|
45
|
-
})
|
|
46
|
-
|
|
47
|
-
const AsyncFooter = defineAsyncComponent({
|
|
48
|
-
loader: () => import('./Footer.vue'),
|
|
49
|
-
hydrate: hydrateOnIdle(5000),
|
|
50
|
-
})
|
|
51
|
-
</script>
|
|
52
|
-
```
|
|
53
|
-
|
|
54
|
-
## Prevent Loading Spinner Flicker
|
|
55
|
-
|
|
56
|
-
Avoid showing loading UI immediately for components that usually resolve quickly.
|
|
57
|
-
|
|
58
|
-
**BAD:**
|
|
59
|
-
|
|
60
|
-
```vue
|
|
61
|
-
<script setup lang="ts">
|
|
62
|
-
import { defineAsyncComponent } from 'vue'
|
|
63
|
-
|
|
64
|
-
import LoadingSpinner from './LoadingSpinner.vue'
|
|
65
|
-
|
|
66
|
-
const AsyncDashboard = defineAsyncComponent({
|
|
67
|
-
loader: () => import('./Dashboard.vue'),
|
|
68
|
-
loadingComponent: LoadingSpinner,
|
|
69
|
-
delay: 0,
|
|
70
|
-
})
|
|
71
|
-
</script>
|
|
72
|
-
```
|
|
73
|
-
|
|
74
|
-
**GOOD:**
|
|
75
|
-
|
|
76
|
-
```vue
|
|
77
|
-
<script setup lang="ts">
|
|
78
|
-
import { defineAsyncComponent } from 'vue'
|
|
79
|
-
|
|
80
|
-
import ErrorDisplay from './ErrorDisplay.vue'
|
|
81
|
-
import LoadingSpinner from './LoadingSpinner.vue'
|
|
82
|
-
|
|
83
|
-
const AsyncDashboard = defineAsyncComponent({
|
|
84
|
-
loader: () => import('./Dashboard.vue'),
|
|
85
|
-
loadingComponent: LoadingSpinner,
|
|
86
|
-
errorComponent: ErrorDisplay,
|
|
87
|
-
delay: 200,
|
|
88
|
-
timeout: 30000,
|
|
89
|
-
})
|
|
90
|
-
</script>
|
|
91
|
-
```
|
|
92
|
-
|
|
93
|
-
## Delay Guidelines
|
|
94
|
-
|
|
95
|
-
| Scenario | Recommended Delay |
|
|
96
|
-
| ----------------------------- | ----------------- |
|
|
97
|
-
| Small component, fast network | `200ms` |
|
|
98
|
-
| Known heavy component | `100ms` |
|
|
99
|
-
| Background or non-critical UI | `300-500ms` |
|