@dianzhong/create-harness-app 0.1.0

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.
Files changed (95) hide show
  1. package/dist/index.mjs +412 -0
  2. package/package.json +29 -0
  3. package/templates/axios/.env.example +2 -0
  4. package/templates/axios/src/api/auth.ts +19 -0
  5. package/templates/axios/src/api/request.ts +61 -0
  6. package/templates/axios/src/types/api.ts +26 -0
  7. package/templates/axios/src/utils/auth.ts +5 -0
  8. package/templates/axios/src/utils/storage.ts +17 -0
  9. package/templates/harness/full/.agents/skills/find-skills/SKILL.md +143 -0
  10. package/templates/harness/full/.agents/skills/vue-best-practices/LICENSE.md +21 -0
  11. package/templates/harness/full/.agents/skills/vue-best-practices/SKILL.md +155 -0
  12. package/templates/harness/full/.agents/skills/vue-best-practices/SYNC.md +5 -0
  13. package/templates/harness/full/.agents/skills/vue-best-practices/references/animation-class-based-technique.md +258 -0
  14. package/templates/harness/full/.agents/skills/vue-best-practices/references/animation-state-driven-technique.md +287 -0
  15. package/templates/harness/full/.agents/skills/vue-best-practices/references/component-async.md +99 -0
  16. package/templates/harness/full/.agents/skills/vue-best-practices/references/component-data-flow.md +313 -0
  17. package/templates/harness/full/.agents/skills/vue-best-practices/references/component-fallthrough-attrs.md +179 -0
  18. package/templates/harness/full/.agents/skills/vue-best-practices/references/component-keep-alive.md +139 -0
  19. package/templates/harness/full/.agents/skills/vue-best-practices/references/component-slots.md +226 -0
  20. package/templates/harness/full/.agents/skills/vue-best-practices/references/component-suspense.md +231 -0
  21. package/templates/harness/full/.agents/skills/vue-best-practices/references/component-teleport.md +110 -0
  22. package/templates/harness/full/.agents/skills/vue-best-practices/references/component-transition-group.md +131 -0
  23. package/templates/harness/full/.agents/skills/vue-best-practices/references/component-transition.md +135 -0
  24. package/templates/harness/full/.agents/skills/vue-best-practices/references/composables.md +303 -0
  25. package/templates/harness/full/.agents/skills/vue-best-practices/references/directives.md +168 -0
  26. package/templates/harness/full/.agents/skills/vue-best-practices/references/perf-avoid-component-abstraction-in-lists.md +177 -0
  27. package/templates/harness/full/.agents/skills/vue-best-practices/references/perf-v-once-v-memo-directives.md +185 -0
  28. package/templates/harness/full/.agents/skills/vue-best-practices/references/perf-virtualize-large-lists.md +182 -0
  29. package/templates/harness/full/.agents/skills/vue-best-practices/references/plugins.md +178 -0
  30. package/templates/harness/full/.agents/skills/vue-best-practices/references/reactivity.md +371 -0
  31. package/templates/harness/full/.agents/skills/vue-best-practices/references/render-functions.md +227 -0
  32. package/templates/harness/full/.agents/skills/vue-best-practices/references/sfc.md +355 -0
  33. package/templates/harness/full/.agents/skills/vue-best-practices/references/state-management.md +138 -0
  34. package/templates/harness/full/.agents/skills/vue-best-practices/references/updated-hook-performance.md +193 -0
  35. package/templates/harness/full/.claude/agents/code-reviewer.md +109 -0
  36. package/templates/harness/full/.claude/agents/harness-reviewer.md +51 -0
  37. package/templates/harness/full/.claude/hooks/guard-tool.cjs +234 -0
  38. package/templates/harness/full/.claude/hooks/notify.cjs +168 -0
  39. package/templates/harness/full/.claude/hooks/quality-gate.cjs +135 -0
  40. package/templates/harness/full/.claude/rules/delivery.md +66 -0
  41. package/templates/harness/full/.claude/rules/formatting.md +7 -0
  42. package/templates/harness/full/.claude/rules/git.md +8 -0
  43. package/templates/harness/full/.claude/rules/skills-mcp.md +13 -0
  44. package/templates/harness/full/.claude/rules/vue.md +227 -0
  45. package/templates/harness/full/.claude/settings.json +123 -0
  46. package/templates/harness/full/.claude/skills/find-skills/SKILL.md +143 -0
  47. package/templates/harness/full/.claude/skills/vue-best-practices/LICENSE.md +21 -0
  48. package/templates/harness/full/.claude/skills/vue-best-practices/SKILL.md +155 -0
  49. package/templates/harness/full/.claude/skills/vue-best-practices/SYNC.md +5 -0
  50. package/templates/harness/full/.claude/skills/vue-best-practices/references/animation-class-based-technique.md +258 -0
  51. package/templates/harness/full/.claude/skills/vue-best-practices/references/animation-state-driven-technique.md +287 -0
  52. package/templates/harness/full/.claude/skills/vue-best-practices/references/component-async.md +99 -0
  53. package/templates/harness/full/.claude/skills/vue-best-practices/references/component-data-flow.md +313 -0
  54. package/templates/harness/full/.claude/skills/vue-best-practices/references/component-fallthrough-attrs.md +179 -0
  55. package/templates/harness/full/.claude/skills/vue-best-practices/references/component-keep-alive.md +139 -0
  56. package/templates/harness/full/.claude/skills/vue-best-practices/references/component-slots.md +226 -0
  57. package/templates/harness/full/.claude/skills/vue-best-practices/references/component-suspense.md +231 -0
  58. package/templates/harness/full/.claude/skills/vue-best-practices/references/component-teleport.md +110 -0
  59. package/templates/harness/full/.claude/skills/vue-best-practices/references/component-transition-group.md +131 -0
  60. package/templates/harness/full/.claude/skills/vue-best-practices/references/component-transition.md +135 -0
  61. package/templates/harness/full/.claude/skills/vue-best-practices/references/composables.md +303 -0
  62. package/templates/harness/full/.claude/skills/vue-best-practices/references/directives.md +168 -0
  63. package/templates/harness/full/.claude/skills/vue-best-practices/references/perf-avoid-component-abstraction-in-lists.md +177 -0
  64. package/templates/harness/full/.claude/skills/vue-best-practices/references/perf-v-once-v-memo-directives.md +185 -0
  65. package/templates/harness/full/.claude/skills/vue-best-practices/references/perf-virtualize-large-lists.md +182 -0
  66. package/templates/harness/full/.claude/skills/vue-best-practices/references/plugins.md +178 -0
  67. package/templates/harness/full/.claude/skills/vue-best-practices/references/reactivity.md +371 -0
  68. package/templates/harness/full/.claude/skills/vue-best-practices/references/render-functions.md +227 -0
  69. package/templates/harness/full/.claude/skills/vue-best-practices/references/sfc.md +355 -0
  70. package/templates/harness/full/.claude/skills/vue-best-practices/references/state-management.md +138 -0
  71. package/templates/harness/full/.claude/skills/vue-best-practices/references/updated-hook-performance.md +193 -0
  72. package/templates/harness/full/.editorconfig +8 -0
  73. package/templates/harness/full/.husky/commit-msg +1 -0
  74. package/templates/harness/full/.husky/pre-commit +1 -0
  75. package/templates/harness/full/.lintstagedrc.json +4 -0
  76. package/templates/harness/full/.nvmrc +1 -0
  77. package/templates/harness/full/.oxlintrc.json +11 -0
  78. package/templates/harness/full/.prettierrc.json +6 -0
  79. package/templates/harness/full/AGENTS.md +3 -0
  80. package/templates/harness/full/CLAUDE.md +28 -0
  81. package/templates/harness/full/GEMINI.md +3 -0
  82. package/templates/harness/full/commitlint.config.ts +3 -0
  83. package/templates/harness/full/docs/ai-harness.md +77 -0
  84. package/templates/harness/full/docs/delivery-template.md +66 -0
  85. package/templates/harness/full/docs/git.md +24 -0
  86. package/templates/harness/full/docs/harness-quick-reference.md +89 -0
  87. package/templates/harness/full/docs/review-checklist.md +49 -0
  88. package/templates/harness/full/scripts/harness-hooks.test.mjs +218 -0
  89. package/templates/harness/full/scripts/verify-skills.mjs +248 -0
  90. package/templates/harness/full/scripts/verify-skills.test.mjs +72 -0
  91. package/templates/harness/full/skills-lock.json +50 -0
  92. package/templates/harness/minimal/.claude/hooks/guard-tool.cjs +234 -0
  93. package/templates/harness/minimal/.claude/hooks/quality-gate.cjs +135 -0
  94. package/templates/harness/minimal/.claude/settings.json +27 -0
  95. package/templates/harness/minimal/CLAUDE.md +12 -0
@@ -0,0 +1,287 @@
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
+ ```
@@ -0,0 +1,99 @@
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` |
@@ -0,0 +1,313 @@
1
+ ---
2
+ title: Component Data Flow Best Practices
3
+ impact: HIGH
4
+ impactDescription: Clear data flow between components prevents state bugs, stale UI, and brittle coupling
5
+ type: best-practice
6
+ tags: [vue3, props, emits, v-model, provide-inject, data-flow, typescript]
7
+ ---
8
+
9
+ # Component Data Flow Best Practices
10
+
11
+ **Impact: HIGH** - Vue components stay reliable when data flow is explicit: props go down, events go up, `v-model` handles two-way bindings, and provide/inject supports cross-tree dependencies. Blurring these boundaries leads to stale state, hidden coupling, and hard-to-debug UI.
12
+
13
+ The main principle of data flow in Vue.js is **Props Down / Events Up**. This is the most maintainable default, and one-way flow scales well.
14
+
15
+ ## Task List
16
+
17
+ - Treat props as read-only inputs
18
+ - Use props/emit for component communication; reserve refs for imperative actions
19
+ - When refs are required for imperative APIs, type them with template refs
20
+ - Emit events instead of mutating parent state directly
21
+ - Use `defineModel` for v-model in modern Vue (3.4+)
22
+ - Handle v-model modifiers deliberately in child components
23
+ - Use symbols for provide/inject keys to avoid props drilling (over ~3 layers)
24
+ - Keep mutations in the provider or expose explicit actions
25
+ - In TypeScript projects, prefer type-based `defineProps`, `defineEmits`, and `InjectionKey`
26
+
27
+ ## Props: One-Way Data Down
28
+
29
+ Props are inputs. Do not mutate them in the child.
30
+
31
+ **BAD:**
32
+
33
+ ```vue
34
+ <script setup>
35
+ const props = defineProps({ count: Number })
36
+
37
+ function increment() {
38
+ props.count++
39
+ }
40
+ </script>
41
+ ```
42
+
43
+ **GOOD:**
44
+
45
+ If state needs to change, emit an event, use `v-model` or create a local copy.
46
+
47
+ ## Prefer props/emit over component refs
48
+
49
+ **BAD:**
50
+
51
+ ```vue
52
+ <script setup>
53
+ import { ref } from 'vue'
54
+
55
+ import UserForm from './UserForm.vue'
56
+
57
+ const formRef = ref(null)
58
+
59
+ function submitForm() {
60
+ if (formRef.value.isValid) {
61
+ formRef.value.submit()
62
+ }
63
+ }
64
+ </script>
65
+
66
+ <template>
67
+ <UserForm ref="formRef" />
68
+ <button @click="submitForm">Submit</button>
69
+ </template>
70
+ ```
71
+
72
+ **GOOD:**
73
+
74
+ ```vue
75
+ <script setup>
76
+ import UserForm from './UserForm.vue'
77
+
78
+ function handleSubmit(formData) {
79
+ api.submit(formData)
80
+ }
81
+ </script>
82
+
83
+ <template>
84
+ <UserForm @submit="handleSubmit" />
85
+ </template>
86
+ ```
87
+
88
+ ## Type component refs when imperative access is required
89
+
90
+ Prefer props/emits by default. When a parent must call an exposed child method, type the ref explicitly and expose only the intended API from the child with `defineExpose`.
91
+
92
+ **BAD:**
93
+
94
+ ```vue
95
+ <script setup lang="ts">
96
+ import { onMounted, ref } from 'vue'
97
+
98
+ import DialogPanel from './DialogPanel.vue'
99
+
100
+ const panelRef = ref(null)
101
+
102
+ onMounted(() => {
103
+ panelRef.value.open()
104
+ })
105
+ </script>
106
+
107
+ <template>
108
+ <DialogPanel ref="panelRef" />
109
+ </template>
110
+ ```
111
+
112
+ **GOOD:**
113
+
114
+ ```vue
115
+ <!-- DialogPanel.vue -->
116
+ <script setup lang="ts">
117
+ function open() {}
118
+
119
+ defineExpose({ open })
120
+ </script>
121
+ ```
122
+
123
+ ```vue
124
+ <!-- Parent.vue -->
125
+ <script setup lang="ts">
126
+ import { onMounted, useTemplateRef } from 'vue'
127
+
128
+ import DialogPanel from './DialogPanel.vue'
129
+
130
+ // Vue 3.5+ with useTemplateRef
131
+ const panelRef = useTemplateRef('panelRef')
132
+
133
+ // Before Vue 3.5 with manual typing and ref
134
+ // const panelRef = ref<InstanceType<typeof DialogPanel> | null>(null)
135
+
136
+ onMounted(() => {
137
+ panelRef.value?.open()
138
+ })
139
+ </script>
140
+
141
+ <template>
142
+ <DialogPanel ref="panelRef" />
143
+ </template>
144
+ ```
145
+
146
+ ## Emits: Explicit Events Up
147
+
148
+ Component events do not bubble. If a parent needs to know about an event, re-emit it explicitly.
149
+
150
+ **BAD:**
151
+
152
+ ```vue
153
+ <!-- Parent expects "saved" from grandchild, but it won't bubble -->
154
+ <Child @saved="onSaved" />
155
+ ```
156
+
157
+ **GOOD:**
158
+
159
+ ```vue
160
+ <!-- Child.vue -->
161
+ <script setup>
162
+ const emit = defineEmits(['saved'])
163
+
164
+ function onGrandchildSaved(payload) {
165
+ emit('saved', payload)
166
+ }
167
+ </script>
168
+
169
+ <template>
170
+ <Grandchild @saved="onGrandchildSaved" />
171
+ </template>
172
+ ```
173
+
174
+ **Event naming:** use kebab-case in templates and camelCase in script:
175
+
176
+ ```vue
177
+ <script setup>
178
+ const emit = defineEmits(['updateUser'])
179
+ </script>
180
+
181
+ <template>
182
+ <ProfileForm @update-user="emit('updateUser', $event)" />
183
+ </template>
184
+ ```
185
+
186
+ ## `v-model`: Predictable Two-Way Bindings
187
+
188
+ Use `defineModel` by default for component bindings and emit updates on input. Only use the `modelValue` + `update:modelValue` pattern if you are on Vue < 3.4.
189
+
190
+ **BAD:**
191
+
192
+ ```vue
193
+ <script setup>
194
+ const props = defineProps({ value: String })
195
+ </script>
196
+
197
+ <template>
198
+ <input :value="props.value" @input="$emit('input', $event.target.value)" />
199
+ </template>
200
+ ```
201
+
202
+ **GOOD (Vue 3.4+):**
203
+
204
+ ```vue
205
+ <script setup>
206
+ const model = defineModel({ type: String })
207
+ </script>
208
+
209
+ <template>
210
+ <input v-model="model" />
211
+ </template>
212
+ ```
213
+
214
+ **GOOD (Vue < 3.4):**
215
+
216
+ ```vue
217
+ <script setup>
218
+ const props = defineProps({ modelValue: String })
219
+ const emit = defineEmits(['update:modelValue'])
220
+ </script>
221
+
222
+ <template>
223
+ <input :value="props.modelValue" @input="emit('update:modelValue', $event.target.value)" />
224
+ </template>
225
+ ```
226
+
227
+ If you need the updated value immediately after a change, use the input event value or `nextTick` in the parent.
228
+
229
+ ## Provide/Inject: Shared Context Without Prop Drilling
230
+
231
+ Use provide/inject for cross-tree state, but keep mutations centralized in the provider and expose explicit actions.
232
+
233
+ **BAD:**
234
+
235
+ ```vue
236
+ // Provider.vue provide('theme', reactive({ dark: false })) // Consumer.vue const theme =
237
+ inject('theme') // Mutating shared state from any depth becomes hard to track theme.dark = true
238
+ ```
239
+
240
+ **GOOD:**
241
+
242
+ ```vue
243
+ // Provider.vue const theme = reactive({ dark: false }) const toggleTheme = () => { theme.dark =
244
+ !theme.dark } provide(themeKey, readonly(theme)) provide(themeActionsKey, { toggleTheme }) //
245
+ Consumer.vue const theme = inject(themeKey) const { toggleTheme } = inject(themeActionsKey)
246
+ ```
247
+
248
+ Use symbols for keys to avoid collisions in large apps:
249
+
250
+ ```ts
251
+ export const themeKey = Symbol('theme')
252
+ export const themeActionsKey = Symbol('theme-actions')
253
+ ```
254
+
255
+ ## Use TypeScript Contracts for Public Component APIs
256
+
257
+ In TypeScript projects, type component boundaries directly with `defineProps`, `defineEmits`, and `InjectionKey` so invalid payloads and mismatched injections fail at compile time.
258
+
259
+ **BAD:**
260
+
261
+ ```vue
262
+ <script setup lang="ts">
263
+ import { inject } from 'vue'
264
+
265
+ const props = defineProps({
266
+ userId: String,
267
+ })
268
+
269
+ const emit = defineEmits(['save'])
270
+ const settings = inject('settings')
271
+
272
+ // Payload shape is not checked here
273
+ emit('save', 123)
274
+
275
+ // Key is string-based and not type-safe
276
+ settings?.theme = 'dark'
277
+ </script>
278
+ ```
279
+
280
+ **GOOD:**
281
+
282
+ ```vue
283
+ <script setup lang="ts">
284
+ import type { InjectionKey } from 'vue'
285
+
286
+ import { inject, provide } from 'vue'
287
+
288
+ interface Props {
289
+ userId: string
290
+ }
291
+
292
+ interface Emits {
293
+ save: [payload: { id: string; draft: boolean }]
294
+ }
295
+
296
+ interface Settings {
297
+ theme: 'light' | 'dark'
298
+ }
299
+
300
+ const props = defineProps<Props>()
301
+
302
+ const emit = defineEmits<Emits>()
303
+
304
+ const settingsKey: InjectionKey<Settings> = Symbol('settings')
305
+
306
+ provide(settingsKey, { theme: 'light' })
307
+
308
+ const settings = inject(settingsKey)
309
+ if (settings) {
310
+ emit('save', { id: props.userId, draft: false })
311
+ }
312
+ </script>
313
+ ```