@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,131 @@
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
+ ```
@@ -0,0 +1,135 @@
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
+ ```
@@ -0,0 +1,303 @@
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
+ ```