@dianzhong/create-harness-app 0.1.1 → 0.1.3
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/README.md +68 -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,131 +0,0 @@
|
|
|
1
|
-
---
|
|
2
|
-
title: TransitionGroup Component Best Practices
|
|
3
|
-
impact: MEDIUM
|
|
4
|
-
impactDescription: TransitionGroup animates list items; missing keys or misuse leads to broken list transitions
|
|
5
|
-
type: best-practice
|
|
6
|
-
tags: [vue3, transition-group, animation, lists, keys]
|
|
7
|
-
---
|
|
8
|
-
|
|
9
|
-
# TransitionGroup Component Best Practices
|
|
10
|
-
|
|
11
|
-
**Impact: MEDIUM** - `<TransitionGroup>` animates lists of items entering, leaving, and moving. Use it for `v-for` lists or dynamic collections where individual items change over time.
|
|
12
|
-
|
|
13
|
-
## Task List
|
|
14
|
-
|
|
15
|
-
- Use `<TransitionGroup>` only for lists and repeated items
|
|
16
|
-
- Provide unique, stable keys for every direct child
|
|
17
|
-
- Use `tag` when you need semantic or layout wrappers
|
|
18
|
-
- Avoid the `mode` prop (not supported)
|
|
19
|
-
- Use JavaScript hooks for staggered effects
|
|
20
|
-
|
|
21
|
-
## Use TransitionGroup for Lists
|
|
22
|
-
|
|
23
|
-
`<TransitionGroup>` is designed for list items. Use `tag` to control the wrapper element when needed.
|
|
24
|
-
|
|
25
|
-
**BAD:**
|
|
26
|
-
|
|
27
|
-
```vue
|
|
28
|
-
<template>
|
|
29
|
-
<TransitionGroup name="fade">
|
|
30
|
-
<ComponentA />
|
|
31
|
-
<ComponentB />
|
|
32
|
-
</TransitionGroup>
|
|
33
|
-
</template>
|
|
34
|
-
```
|
|
35
|
-
|
|
36
|
-
**GOOD:**
|
|
37
|
-
|
|
38
|
-
```vue
|
|
39
|
-
<template>
|
|
40
|
-
<TransitionGroup name="list" tag="ul">
|
|
41
|
-
<li v-for="item in items" :key="item.id">
|
|
42
|
-
{{ item.name }}
|
|
43
|
-
</li>
|
|
44
|
-
</TransitionGroup>
|
|
45
|
-
</template>
|
|
46
|
-
```
|
|
47
|
-
|
|
48
|
-
## Always Provide Stable Keys
|
|
49
|
-
|
|
50
|
-
Keys are required. Without stable keys, Vue cannot track item positions and animations break.
|
|
51
|
-
|
|
52
|
-
**BAD:**
|
|
53
|
-
|
|
54
|
-
```vue
|
|
55
|
-
<template>
|
|
56
|
-
<TransitionGroup name="list" tag="ul">
|
|
57
|
-
<li v-for="(item, index) in items" :key="index">
|
|
58
|
-
{{ item.name }}
|
|
59
|
-
</li>
|
|
60
|
-
</TransitionGroup>
|
|
61
|
-
</template>
|
|
62
|
-
```
|
|
63
|
-
|
|
64
|
-
**GOOD:**
|
|
65
|
-
|
|
66
|
-
```vue
|
|
67
|
-
<template>
|
|
68
|
-
<TransitionGroup name="list" tag="ul">
|
|
69
|
-
<li v-for="item in items" :key="item.id">
|
|
70
|
-
{{ item.name }}
|
|
71
|
-
</li>
|
|
72
|
-
</TransitionGroup>
|
|
73
|
-
</template>
|
|
74
|
-
```
|
|
75
|
-
|
|
76
|
-
## Do Not Use `mode` on TransitionGroup
|
|
77
|
-
|
|
78
|
-
`mode` is only for `<Transition>` because it swaps a single element. Use `<Transition>` if you need in/out sequencing.
|
|
79
|
-
|
|
80
|
-
**BAD:**
|
|
81
|
-
|
|
82
|
-
```vue
|
|
83
|
-
<template>
|
|
84
|
-
<TransitionGroup name="list" tag="div" mode="out-in">
|
|
85
|
-
<div v-for="item in items" :key="item.id">
|
|
86
|
-
{{ item.name }}
|
|
87
|
-
</div>
|
|
88
|
-
</TransitionGroup>
|
|
89
|
-
</template>
|
|
90
|
-
```
|
|
91
|
-
|
|
92
|
-
**GOOD:**
|
|
93
|
-
|
|
94
|
-
```vue
|
|
95
|
-
<template>
|
|
96
|
-
<Transition name="fade" mode="out-in">
|
|
97
|
-
<component :is="currentView" :key="currentView" />
|
|
98
|
-
</Transition>
|
|
99
|
-
</template>
|
|
100
|
-
```
|
|
101
|
-
|
|
102
|
-
## Stagger List Animations with Data Attributes
|
|
103
|
-
|
|
104
|
-
For cascading list animations, pass the index to JavaScript hooks and compute delay per item.
|
|
105
|
-
|
|
106
|
-
```vue
|
|
107
|
-
<script setup>
|
|
108
|
-
function onBeforeEnter(el) {
|
|
109
|
-
el.style.opacity = 0
|
|
110
|
-
el.style.transform = 'translateY(12px)'
|
|
111
|
-
}
|
|
112
|
-
|
|
113
|
-
function onEnter(el, done) {
|
|
114
|
-
const delay = Number(el.dataset.index) * 80
|
|
115
|
-
setTimeout(() => {
|
|
116
|
-
el.style.transition = 'all 0.25s ease'
|
|
117
|
-
el.style.opacity = 1
|
|
118
|
-
el.style.transform = 'translateY(0)'
|
|
119
|
-
setTimeout(done, 250)
|
|
120
|
-
}, delay)
|
|
121
|
-
}
|
|
122
|
-
</script>
|
|
123
|
-
|
|
124
|
-
<template>
|
|
125
|
-
<TransitionGroup tag="ul" :css="false" @before-enter="onBeforeEnter" @enter="onEnter">
|
|
126
|
-
<li v-for="(item, index) in items" :key="item.id" :data-index="index">
|
|
127
|
-
{{ item.name }}
|
|
128
|
-
</li>
|
|
129
|
-
</TransitionGroup>
|
|
130
|
-
</template>
|
|
131
|
-
```
|
package/templates/harness/full/.agents/skills/vue-best-practices/references/component-transition.md
DELETED
|
@@ -1,135 +0,0 @@
|
|
|
1
|
-
---
|
|
2
|
-
title: Transition Component Best Practices
|
|
3
|
-
impact: MEDIUM
|
|
4
|
-
impactDescription: Transition animates a single element or component; incorrect structure or keys prevent animations
|
|
5
|
-
type: best-practice
|
|
6
|
-
tags: [vue3, transition, animation, performance, keys]
|
|
7
|
-
---
|
|
8
|
-
|
|
9
|
-
# Transition Component Best Practices
|
|
10
|
-
|
|
11
|
-
**Impact: MEDIUM** - `<Transition>` animates entering/leaving of a single element or component. It is ideal for toggling UI states, swapping views, or animating one component at a time.
|
|
12
|
-
|
|
13
|
-
## Task List
|
|
14
|
-
|
|
15
|
-
- Wrap a single element or component inside `<Transition>`
|
|
16
|
-
- Provide a `key` when switching between same element types
|
|
17
|
-
- Use `mode="out-in"` when you need sequential swaps
|
|
18
|
-
- Prefer `transform` and `opacity` for smooth animations
|
|
19
|
-
|
|
20
|
-
## Use Transition for a Single Root Element
|
|
21
|
-
|
|
22
|
-
`<Transition>` only supports one direct child. Wrap multiple nodes in a single element or component.
|
|
23
|
-
|
|
24
|
-
**BAD:**
|
|
25
|
-
|
|
26
|
-
```vue
|
|
27
|
-
<template>
|
|
28
|
-
<Transition name="fade">
|
|
29
|
-
<h3>Title</h3>
|
|
30
|
-
<p>Description</p>
|
|
31
|
-
</Transition>
|
|
32
|
-
</template>
|
|
33
|
-
```
|
|
34
|
-
|
|
35
|
-
**GOOD:**
|
|
36
|
-
|
|
37
|
-
```vue
|
|
38
|
-
<template>
|
|
39
|
-
<Transition name="fade">
|
|
40
|
-
<div>
|
|
41
|
-
<h3>Title</h3>
|
|
42
|
-
<p>Description</p>
|
|
43
|
-
</div>
|
|
44
|
-
</Transition>
|
|
45
|
-
</template>
|
|
46
|
-
```
|
|
47
|
-
|
|
48
|
-
## Force Transitions Between Same Element Types
|
|
49
|
-
|
|
50
|
-
Vue reuses the same DOM element when the tag type does not change. Add `key` so Vue treats it as a new element and triggers enter/leave.
|
|
51
|
-
|
|
52
|
-
**BAD:**
|
|
53
|
-
|
|
54
|
-
```vue
|
|
55
|
-
<template>
|
|
56
|
-
<Transition name="fade">
|
|
57
|
-
<p v-if="isActive">Active</p>
|
|
58
|
-
<p v-else>Inactive</p>
|
|
59
|
-
</Transition>
|
|
60
|
-
</template>
|
|
61
|
-
```
|
|
62
|
-
|
|
63
|
-
**GOOD:**
|
|
64
|
-
|
|
65
|
-
```vue
|
|
66
|
-
<template>
|
|
67
|
-
<Transition name="fade" mode="out-in">
|
|
68
|
-
<p v-if="isActive" key="active">Active</p>
|
|
69
|
-
<p v-else key="inactive">Inactive</p>
|
|
70
|
-
</Transition>
|
|
71
|
-
</template>
|
|
72
|
-
```
|
|
73
|
-
|
|
74
|
-
## Use `mode` to Avoid Overlap During Swaps
|
|
75
|
-
|
|
76
|
-
When swapping components or views, use `mode="out-in"` to prevent both from being visible at the same time.
|
|
77
|
-
|
|
78
|
-
**BAD:**
|
|
79
|
-
|
|
80
|
-
```vue
|
|
81
|
-
<template>
|
|
82
|
-
<Transition name="fade">
|
|
83
|
-
<component :is="currentView" />
|
|
84
|
-
</Transition>
|
|
85
|
-
</template>
|
|
86
|
-
```
|
|
87
|
-
|
|
88
|
-
**GOOD:**
|
|
89
|
-
|
|
90
|
-
```vue
|
|
91
|
-
<template>
|
|
92
|
-
<Transition name="fade" mode="out-in">
|
|
93
|
-
<component :is="currentView" :key="currentView" />
|
|
94
|
-
</Transition>
|
|
95
|
-
</template>
|
|
96
|
-
```
|
|
97
|
-
|
|
98
|
-
## Animate `transform` and `opacity` for Performance
|
|
99
|
-
|
|
100
|
-
Avoid layout-triggering properties such as `height`, `margin`, or `top`. Use `transform` and `opacity` for smooth, GPU-friendly transitions.
|
|
101
|
-
|
|
102
|
-
**BAD:**
|
|
103
|
-
|
|
104
|
-
```css
|
|
105
|
-
.slide-enter-active,
|
|
106
|
-
.slide-leave-active {
|
|
107
|
-
transition: height 0.3s ease;
|
|
108
|
-
}
|
|
109
|
-
|
|
110
|
-
.slide-enter-from,
|
|
111
|
-
.slide-leave-to {
|
|
112
|
-
height: 0;
|
|
113
|
-
}
|
|
114
|
-
```
|
|
115
|
-
|
|
116
|
-
**GOOD:**
|
|
117
|
-
|
|
118
|
-
```css
|
|
119
|
-
.slide-enter-active,
|
|
120
|
-
.slide-leave-active {
|
|
121
|
-
transition:
|
|
122
|
-
transform 0.3s ease,
|
|
123
|
-
opacity 0.3s ease;
|
|
124
|
-
}
|
|
125
|
-
|
|
126
|
-
.slide-enter-from {
|
|
127
|
-
transform: translateX(-12px);
|
|
128
|
-
opacity: 0;
|
|
129
|
-
}
|
|
130
|
-
|
|
131
|
-
.slide-leave-to {
|
|
132
|
-
transform: translateX(12px);
|
|
133
|
-
opacity: 0;
|
|
134
|
-
}
|
|
135
|
-
```
|
|
@@ -1,303 +0,0 @@
|
|
|
1
|
-
---
|
|
2
|
-
title: Composable Organization Patterns
|
|
3
|
-
impact: MEDIUM
|
|
4
|
-
impactDescription: Well-structured composables improve maintainability, reusability, and update performance
|
|
5
|
-
type: best-practice
|
|
6
|
-
tags: [vue3, composables, composition-api, code-organization, api-design, readonly, utilities]
|
|
7
|
-
---
|
|
8
|
-
|
|
9
|
-
# Composable Organization Patterns
|
|
10
|
-
|
|
11
|
-
**Impact: MEDIUM** - Treat composables as reusable, stateful building blocks and keep their code organized by feature concern. This keeps large components maintainable and prevents hard-to-debug mutation and API design issues.
|
|
12
|
-
|
|
13
|
-
## Task List
|
|
14
|
-
|
|
15
|
-
- Compose complex behavior from small, focused composables
|
|
16
|
-
- Use options objects for composables with multiple optional parameters
|
|
17
|
-
- Return readonly state when updates must flow through explicit actions
|
|
18
|
-
- Keep pure utility functions as plain utilities, not composables
|
|
19
|
-
- Organize composable and component code by feature concern, and extract composables when components grow
|
|
20
|
-
|
|
21
|
-
## Compose Composables from Smaller Primitives
|
|
22
|
-
|
|
23
|
-
**BAD:**
|
|
24
|
-
|
|
25
|
-
```vue
|
|
26
|
-
<script setup>
|
|
27
|
-
import { computed, onMounted, onUnmounted, ref } from 'vue'
|
|
28
|
-
|
|
29
|
-
const x = ref(0)
|
|
30
|
-
const y = ref(0)
|
|
31
|
-
const inside = ref(false)
|
|
32
|
-
const el = ref(null)
|
|
33
|
-
|
|
34
|
-
function onMove(e) {
|
|
35
|
-
x.value = e.pageX
|
|
36
|
-
y.value = e.pageY
|
|
37
|
-
if (!el.value) return
|
|
38
|
-
const r = el.value.getBoundingClientRect()
|
|
39
|
-
inside.value = x.value >= r.left && x.value <= r.right && y.value >= r.top && y.value <= r.bottom
|
|
40
|
-
}
|
|
41
|
-
|
|
42
|
-
onMounted(() => window.addEventListener('mousemove', onMove))
|
|
43
|
-
onUnmounted(() => window.removeEventListener('mousemove', onMove))
|
|
44
|
-
</script>
|
|
45
|
-
```
|
|
46
|
-
|
|
47
|
-
**GOOD:**
|
|
48
|
-
|
|
49
|
-
```javascript
|
|
50
|
-
// composables/useEventListener.js
|
|
51
|
-
import { onMounted, onUnmounted, toValue } from 'vue'
|
|
52
|
-
|
|
53
|
-
export function useEventListener(target, event, callback) {
|
|
54
|
-
onMounted(() => toValue(target).addEventListener(event, callback))
|
|
55
|
-
onUnmounted(() => toValue(target).removeEventListener(event, callback))
|
|
56
|
-
}
|
|
57
|
-
```
|
|
58
|
-
|
|
59
|
-
```javascript
|
|
60
|
-
// composables/useMouse.js
|
|
61
|
-
import { ref } from 'vue'
|
|
62
|
-
|
|
63
|
-
import { useEventListener } from './useEventListener'
|
|
64
|
-
|
|
65
|
-
export function useMouse() {
|
|
66
|
-
const x = ref(0)
|
|
67
|
-
const y = ref(0)
|
|
68
|
-
|
|
69
|
-
useEventListener(window, 'mousemove', (e) => {
|
|
70
|
-
x.value = e.pageX
|
|
71
|
-
y.value = e.pageY
|
|
72
|
-
})
|
|
73
|
-
|
|
74
|
-
return { x, y }
|
|
75
|
-
}
|
|
76
|
-
```
|
|
77
|
-
|
|
78
|
-
```javascript
|
|
79
|
-
// composables/useMouseInElement.js
|
|
80
|
-
import { computed } from 'vue'
|
|
81
|
-
|
|
82
|
-
import { useMouse } from './useMouse'
|
|
83
|
-
|
|
84
|
-
export function useMouseInElement(elementRef) {
|
|
85
|
-
const { x, y } = useMouse()
|
|
86
|
-
|
|
87
|
-
const isOutside = computed(() => {
|
|
88
|
-
if (!elementRef.value) return true
|
|
89
|
-
const rect = elementRef.value.getBoundingClientRect()
|
|
90
|
-
return (
|
|
91
|
-
x.value < rect.left || x.value > rect.right || y.value < rect.top || y.value > rect.bottom
|
|
92
|
-
)
|
|
93
|
-
})
|
|
94
|
-
|
|
95
|
-
return { x, y, isOutside }
|
|
96
|
-
}
|
|
97
|
-
```
|
|
98
|
-
|
|
99
|
-
## Use Options Object Pattern for Composable Parameters
|
|
100
|
-
|
|
101
|
-
**BAD:**
|
|
102
|
-
|
|
103
|
-
```javascript
|
|
104
|
-
export function useFetch(url, method, headers, timeout, retries, immediate) {
|
|
105
|
-
// hard to read and easy to misorder
|
|
106
|
-
}
|
|
107
|
-
|
|
108
|
-
useFetch('/api/users', 'GET', null, 5000, 3, true)
|
|
109
|
-
```
|
|
110
|
-
|
|
111
|
-
**GOOD:**
|
|
112
|
-
|
|
113
|
-
```javascript
|
|
114
|
-
export function useFetch(url, options = {}) {
|
|
115
|
-
const { method = 'GET', headers = {}, timeout = 30000, retries = 0, immediate = true } = options
|
|
116
|
-
|
|
117
|
-
// implementation
|
|
118
|
-
return { method, headers, timeout, retries, immediate }
|
|
119
|
-
}
|
|
120
|
-
|
|
121
|
-
useFetch('/api/users', {
|
|
122
|
-
method: 'POST',
|
|
123
|
-
timeout: 5000,
|
|
124
|
-
retries: 3,
|
|
125
|
-
})
|
|
126
|
-
```
|
|
127
|
-
|
|
128
|
-
```typescript
|
|
129
|
-
interface UseCounterOptions {
|
|
130
|
-
initial?: number
|
|
131
|
-
min?: number
|
|
132
|
-
max?: number
|
|
133
|
-
step?: number
|
|
134
|
-
}
|
|
135
|
-
|
|
136
|
-
export function useCounter(options: UseCounterOptions = {}) {
|
|
137
|
-
const { initial = 0, min = -Infinity, max = Infinity, step = 1 } = options
|
|
138
|
-
// implementation
|
|
139
|
-
}
|
|
140
|
-
```
|
|
141
|
-
|
|
142
|
-
## Return Readonly State with Explicit Actions
|
|
143
|
-
|
|
144
|
-
**BAD:**
|
|
145
|
-
|
|
146
|
-
```javascript
|
|
147
|
-
export function useCart() {
|
|
148
|
-
const items = ref([])
|
|
149
|
-
const total = computed(() => items.value.reduce((sum, item) => sum + item.price, 0))
|
|
150
|
-
return { items, total } // any consumer can mutate directly
|
|
151
|
-
}
|
|
152
|
-
|
|
153
|
-
const { items } = useCart()
|
|
154
|
-
items.value.push({ id: 1, price: 10 })
|
|
155
|
-
```
|
|
156
|
-
|
|
157
|
-
**GOOD:**
|
|
158
|
-
|
|
159
|
-
```javascript
|
|
160
|
-
import { computed, readonly, ref } from 'vue'
|
|
161
|
-
|
|
162
|
-
export function useCart() {
|
|
163
|
-
const _items = ref([])
|
|
164
|
-
|
|
165
|
-
const total = computed(() =>
|
|
166
|
-
_items.value.reduce((sum, item) => sum + item.price * item.quantity, 0),
|
|
167
|
-
)
|
|
168
|
-
|
|
169
|
-
function addItem(product, quantity = 1) {
|
|
170
|
-
const existing = _items.value.find((item) => item.id === product.id)
|
|
171
|
-
if (existing) {
|
|
172
|
-
existing.quantity += quantity
|
|
173
|
-
return
|
|
174
|
-
}
|
|
175
|
-
_items.value.push({ ...product, quantity })
|
|
176
|
-
}
|
|
177
|
-
|
|
178
|
-
function removeItem(productId) {
|
|
179
|
-
_items.value = _items.value.filter((item) => item.id !== productId)
|
|
180
|
-
}
|
|
181
|
-
|
|
182
|
-
return {
|
|
183
|
-
items: readonly(_items),
|
|
184
|
-
total,
|
|
185
|
-
addItem,
|
|
186
|
-
removeItem,
|
|
187
|
-
}
|
|
188
|
-
}
|
|
189
|
-
```
|
|
190
|
-
|
|
191
|
-
## Keep Utilities as Utilities
|
|
192
|
-
|
|
193
|
-
**BAD:**
|
|
194
|
-
|
|
195
|
-
```javascript
|
|
196
|
-
export function useFormatters() {
|
|
197
|
-
const formatDate = (date) => new Intl.DateTimeFormat('en-US').format(date)
|
|
198
|
-
const formatCurrency = (amount) =>
|
|
199
|
-
new Intl.NumberFormat('en-US', { style: 'currency', currency: 'USD' }).format(amount)
|
|
200
|
-
return { formatDate, formatCurrency }
|
|
201
|
-
}
|
|
202
|
-
|
|
203
|
-
const { formatDate } = useFormatters()
|
|
204
|
-
```
|
|
205
|
-
|
|
206
|
-
**GOOD:**
|
|
207
|
-
|
|
208
|
-
```javascript
|
|
209
|
-
// utils/formatters.js
|
|
210
|
-
export function formatDate(date) {
|
|
211
|
-
return new Intl.DateTimeFormat('en-US').format(date)
|
|
212
|
-
}
|
|
213
|
-
|
|
214
|
-
export function formatCurrency(amount) {
|
|
215
|
-
return new Intl.NumberFormat('en-US', {
|
|
216
|
-
style: 'currency',
|
|
217
|
-
currency: 'USD',
|
|
218
|
-
}).format(amount)
|
|
219
|
-
}
|
|
220
|
-
```
|
|
221
|
-
|
|
222
|
-
```javascript
|
|
223
|
-
// composables/useInvoiceSummary.js
|
|
224
|
-
import { computed } from 'vue'
|
|
225
|
-
|
|
226
|
-
import { formatCurrency } from '@/utils/formatters'
|
|
227
|
-
|
|
228
|
-
export function useInvoiceSummary(invoiceRef) {
|
|
229
|
-
const totalLabel = computed(() => formatCurrency(invoiceRef.value.total))
|
|
230
|
-
return { totalLabel }
|
|
231
|
-
}
|
|
232
|
-
```
|
|
233
|
-
|
|
234
|
-
## Organize Composable and Component Code by Feature Concern
|
|
235
|
-
|
|
236
|
-
**BAD:**
|
|
237
|
-
|
|
238
|
-
```vue
|
|
239
|
-
<script setup>
|
|
240
|
-
import { computed, onMounted, ref, watch } from 'vue'
|
|
241
|
-
|
|
242
|
-
const searchQuery = ref('')
|
|
243
|
-
const items = ref([])
|
|
244
|
-
const selected = ref(null)
|
|
245
|
-
const showModal = ref(false)
|
|
246
|
-
const sortBy = ref('name')
|
|
247
|
-
const filter = ref('all')
|
|
248
|
-
const loading = ref(false)
|
|
249
|
-
|
|
250
|
-
const filtered = computed(() => items.value.filter((i) => i.category === filter.value))
|
|
251
|
-
function openModal() {
|
|
252
|
-
showModal.value = true
|
|
253
|
-
}
|
|
254
|
-
const sorted = computed(() => [...filtered.value].sort(/* ... */))
|
|
255
|
-
watch(searchQuery, () => {
|
|
256
|
-
/* ... */
|
|
257
|
-
})
|
|
258
|
-
onMounted(() => {
|
|
259
|
-
/* ... */
|
|
260
|
-
})
|
|
261
|
-
</script>
|
|
262
|
-
```
|
|
263
|
-
|
|
264
|
-
**GOOD:**
|
|
265
|
-
|
|
266
|
-
```vue
|
|
267
|
-
<script setup>
|
|
268
|
-
import { useItems } from '@/composables/useItems'
|
|
269
|
-
import { useSearch } from '@/composables/useSearch'
|
|
270
|
-
import { useSelectionModal } from '@/composables/useSelectionModal'
|
|
271
|
-
|
|
272
|
-
// Data
|
|
273
|
-
const { items, loading, fetchItems } = useItems()
|
|
274
|
-
|
|
275
|
-
// Search/filter/sort
|
|
276
|
-
const { query, visibleItems } = useSearch(items)
|
|
277
|
-
|
|
278
|
-
// Selection + modal
|
|
279
|
-
const { selectedItem, isModalOpen, selectItem, closeModal } = useSelectionModal()
|
|
280
|
-
</script>
|
|
281
|
-
```
|
|
282
|
-
|
|
283
|
-
```javascript
|
|
284
|
-
// composables/useItems.js
|
|
285
|
-
import { onMounted, ref } from 'vue'
|
|
286
|
-
|
|
287
|
-
export function useItems() {
|
|
288
|
-
const items = ref([])
|
|
289
|
-
const loading = ref(false)
|
|
290
|
-
|
|
291
|
-
async function fetchItems() {
|
|
292
|
-
loading.value = true
|
|
293
|
-
try {
|
|
294
|
-
items.value = await api.getItems()
|
|
295
|
-
} finally {
|
|
296
|
-
loading.value = false
|
|
297
|
-
}
|
|
298
|
-
}
|
|
299
|
-
|
|
300
|
-
onMounted(fetchItems)
|
|
301
|
-
return { items, loading, fetchItems }
|
|
302
|
-
}
|
|
303
|
-
```
|