tribunal-kit 2.4.6 → 3.0.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.
- package/.agent/agents/accessibility-reviewer.md +220 -134
- package/.agent/agents/ai-code-reviewer.md +233 -129
- package/.agent/agents/backend-specialist.md +238 -178
- package/.agent/agents/code-archaeologist.md +181 -119
- package/.agent/agents/database-architect.md +207 -164
- package/.agent/agents/debugger.md +218 -151
- package/.agent/agents/dependency-reviewer.md +136 -55
- package/.agent/agents/devops-engineer.md +238 -175
- package/.agent/agents/documentation-writer.md +221 -137
- package/.agent/agents/explorer-agent.md +180 -142
- package/.agent/agents/frontend-reviewer.md +194 -80
- package/.agent/agents/frontend-specialist.md +237 -188
- package/.agent/agents/game-developer.md +52 -184
- package/.agent/agents/logic-reviewer.md +149 -78
- package/.agent/agents/mobile-developer.md +223 -152
- package/.agent/agents/mobile-reviewer.md +195 -79
- package/.agent/agents/orchestrator.md +211 -170
- package/.agent/agents/penetration-tester.md +174 -131
- package/.agent/agents/performance-optimizer.md +203 -139
- package/.agent/agents/performance-reviewer.md +211 -108
- package/.agent/agents/product-manager.md +162 -108
- package/.agent/agents/project-planner.md +162 -142
- package/.agent/agents/qa-automation-engineer.md +242 -138
- package/.agent/agents/security-auditor.md +194 -170
- package/.agent/agents/seo-specialist.md +213 -132
- package/.agent/agents/sql-reviewer.md +194 -73
- package/.agent/agents/supervisor-agent.md +203 -156
- package/.agent/agents/test-coverage-reviewer.md +193 -81
- package/.agent/agents/type-safety-reviewer.md +208 -65
- package/.agent/scripts/__pycache__/auto_preview.cpython-311.pyc +0 -0
- package/.agent/scripts/__pycache__/bundle_analyzer.cpython-311.pyc +0 -0
- package/.agent/scripts/__pycache__/checklist.cpython-311.pyc +0 -0
- package/.agent/scripts/__pycache__/dependency_analyzer.cpython-311.pyc +0 -0
- package/.agent/scripts/__pycache__/security_scan.cpython-311.pyc +0 -0
- package/.agent/scripts/__pycache__/session_manager.cpython-311.pyc +0 -0
- package/.agent/scripts/__pycache__/skill_integrator.cpython-311.pyc +0 -0
- package/.agent/scripts/__pycache__/swarm_dispatcher.cpython-311.pyc +0 -0
- package/.agent/scripts/__pycache__/test_runner.cpython-311.pyc +0 -0
- package/.agent/scripts/__pycache__/verify_all.cpython-311.pyc +0 -0
- package/.agent/skills/agent-organizer/SKILL.md +126 -132
- package/.agent/skills/ai-prompt-injection-defense/SKILL.md +155 -66
- package/.agent/skills/api-patterns/SKILL.md +289 -257
- package/.agent/skills/api-security-auditor/SKILL.md +172 -70
- package/.agent/skills/app-builder/templates/chrome-extension/TEMPLATE.md +1 -1
- package/.agent/skills/app-builder/templates/electron-desktop/TEMPLATE.md +1 -1
- package/.agent/skills/appflow-wireframe/SKILL.md +107 -100
- package/.agent/skills/architecture/SKILL.md +331 -200
- package/.agent/skills/authentication-best-practices/SKILL.md +168 -67
- package/.agent/skills/bash-linux/SKILL.md +154 -215
- package/.agent/skills/brainstorming/SKILL.md +104 -210
- package/.agent/skills/building-native-ui/SKILL.md +169 -70
- package/.agent/skills/clean-code/SKILL.md +360 -206
- package/.agent/skills/config-validator/SKILL.md +141 -165
- package/.agent/skills/csharp-developer/SKILL.md +528 -107
- package/.agent/skills/database-design/SKILL.md +455 -275
- package/.agent/skills/deployment-procedures/SKILL.md +145 -188
- package/.agent/skills/devops-engineer/SKILL.md +332 -134
- package/.agent/skills/devops-incident-responder/SKILL.md +113 -98
- package/.agent/skills/edge-computing/SKILL.md +157 -213
- package/.agent/skills/extract-design-system/SKILL.md +129 -69
- package/.agent/skills/framer-motion-expert/SKILL.md +939 -0
- package/.agent/skills/game-design-expert/SKILL.md +105 -0
- package/.agent/skills/game-engineering-expert/SKILL.md +122 -0
- package/.agent/skills/geo-fundamentals/SKILL.md +124 -215
- package/.agent/skills/github-operations/SKILL.md +314 -354
- package/.agent/skills/gsap-expert/SKILL.md +901 -0
- package/.agent/skills/i18n-localization/SKILL.md +138 -216
- package/.agent/skills/intelligent-routing/SKILL.md +127 -139
- package/.agent/skills/llm-engineering/SKILL.md +357 -258
- package/.agent/skills/local-first/SKILL.md +154 -203
- package/.agent/skills/mcp-builder/SKILL.md +118 -224
- package/.agent/skills/nextjs-react-expert/SKILL.md +783 -203
- package/.agent/skills/nodejs-best-practices/SKILL.md +559 -280
- package/.agent/skills/observability/SKILL.md +330 -285
- package/.agent/skills/parallel-agents/SKILL.md +122 -181
- package/.agent/skills/performance-profiling/SKILL.md +254 -197
- package/.agent/skills/plan-writing/SKILL.md +118 -188
- package/.agent/skills/platform-engineer/SKILL.md +123 -135
- package/.agent/skills/playwright-best-practices/SKILL.md +157 -76
- package/.agent/skills/powershell-windows/SKILL.md +146 -230
- package/.agent/skills/python-pro/SKILL.md +879 -114
- package/.agent/skills/react-specialist/SKILL.md +931 -108
- package/.agent/skills/realtime-patterns/SKILL.md +304 -296
- package/.agent/skills/rust-pro/SKILL.md +701 -240
- package/.agent/skills/seo-fundamentals/SKILL.md +154 -181
- package/.agent/skills/server-management/SKILL.md +190 -212
- package/.agent/skills/shadcn-ui-expert/SKILL.md +201 -68
- package/.agent/skills/sql-pro/SKILL.md +633 -104
- package/.agent/skills/swiftui-expert/SKILL.md +171 -70
- package/.agent/skills/systematic-debugging/SKILL.md +118 -186
- package/.agent/skills/tailwind-patterns/SKILL.md +576 -232
- package/.agent/skills/tdd-workflow/SKILL.md +137 -209
- package/.agent/skills/testing-patterns/SKILL.md +573 -205
- package/.agent/skills/vue-expert/SKILL.md +964 -119
- package/.agent/skills/vulnerability-scanner/SKILL.md +269 -316
- package/.agent/skills/web-accessibility-auditor/SKILL.md +188 -71
- package/.agent/skills/webapp-testing/SKILL.md +145 -236
- package/.agent/workflows/api-tester.md +151 -279
- package/.agent/workflows/audit.md +138 -168
- package/.agent/workflows/brainstorm.md +110 -146
- package/.agent/workflows/changelog.md +112 -144
- package/.agent/workflows/create.md +124 -139
- package/.agent/workflows/debug.md +189 -196
- package/.agent/workflows/deploy.md +189 -153
- package/.agent/workflows/enhance.md +151 -139
- package/.agent/workflows/fix.md +135 -143
- package/.agent/workflows/generate.md +157 -164
- package/.agent/workflows/migrate.md +160 -163
- package/.agent/workflows/orchestrate.md +168 -151
- package/.agent/workflows/performance-benchmarker.md +123 -305
- package/.agent/workflows/plan.md +173 -151
- package/.agent/workflows/preview.md +80 -137
- package/.agent/workflows/refactor.md +183 -153
- package/.agent/workflows/review-ai.md +129 -140
- package/.agent/workflows/review.md +116 -155
- package/.agent/workflows/session.md +94 -154
- package/.agent/workflows/status.md +79 -125
- package/.agent/workflows/strengthen-skills.md +139 -99
- package/.agent/workflows/swarm.md +179 -194
- package/.agent/workflows/test.md +211 -166
- package/.agent/workflows/tribunal-backend.md +113 -111
- package/.agent/workflows/tribunal-database.md +115 -132
- package/.agent/workflows/tribunal-frontend.md +118 -115
- package/.agent/workflows/tribunal-full.md +133 -136
- package/.agent/workflows/tribunal-mobile.md +119 -123
- package/.agent/workflows/tribunal-performance.md +133 -152
- package/.agent/workflows/ui-ux-pro-max.md +143 -171
- package/README.md +11 -15
- package/package.json +1 -1
- package/.agent/skills/dotnet-core-expert/SKILL.md +0 -103
- package/.agent/skills/framer-motion-animations/SKILL.md +0 -74
- package/.agent/skills/game-development/2d-games/SKILL.md +0 -119
- package/.agent/skills/game-development/3d-games/SKILL.md +0 -135
- package/.agent/skills/game-development/SKILL.md +0 -236
- package/.agent/skills/game-development/game-art/SKILL.md +0 -185
- package/.agent/skills/game-development/game-audio/SKILL.md +0 -190
- package/.agent/skills/game-development/game-design/SKILL.md +0 -129
- package/.agent/skills/game-development/mobile-games/SKILL.md +0 -108
- package/.agent/skills/game-development/multiplayer/SKILL.md +0 -132
- package/.agent/skills/game-development/pc-games/SKILL.md +0 -144
- package/.agent/skills/game-development/vr-ar/SKILL.md +0 -123
- package/.agent/skills/game-development/web-games/SKILL.md +0 -150
|
@@ -1,119 +1,964 @@
|
|
|
1
|
-
---
|
|
2
|
-
name: vue-expert
|
|
3
|
-
description: Vue 3 Composition API
|
|
4
|
-
allowed-tools: Read, Write, Edit, Glob, Grep
|
|
5
|
-
version:
|
|
6
|
-
last-updated: 2026-03-
|
|
7
|
-
applies-to-model: gemini-2.5-pro, claude-3-7-sonnet
|
|
8
|
-
---
|
|
9
|
-
|
|
10
|
-
# Vue Expert
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
---
|
|
96
|
-
|
|
97
|
-
##
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
1
|
+
---
|
|
2
|
+
name: vue-expert
|
|
3
|
+
description: Vue 3.5+ Composition API mastery. Script setup, reactive refs, computed, watchers, composables, Pinia stores, Vue Router 4, Nuxt 4, Teleport, Transition, provide/inject, TypeScript integration, performance optimization, and testing with Vitest. Use when building Vue applications, designing composables, managing state, or implementing Nuxt patterns.
|
|
4
|
+
allowed-tools: Read, Write, Edit, Glob, Grep
|
|
5
|
+
version: 2.0.0
|
|
6
|
+
last-updated: 2026-03-30
|
|
7
|
+
applies-to-model: gemini-2.5-pro, claude-3-7-sonnet
|
|
8
|
+
---
|
|
9
|
+
|
|
10
|
+
# Vue Expert — Vue 3.5+ & Nuxt 4 Mastery
|
|
11
|
+
|
|
12
|
+
> Vue 3.5 is Composition API everywhere. Options API is legacy. Vuex is dead. Pinia is the standard.
|
|
13
|
+
> Every component uses `<script setup>`. Every store uses Pinia. Every composable returns refs. No exceptions.
|
|
14
|
+
|
|
15
|
+
---
|
|
16
|
+
|
|
17
|
+
## Script Setup (Mandatory)
|
|
18
|
+
|
|
19
|
+
```vue
|
|
20
|
+
<script setup lang="ts">
|
|
21
|
+
// <script setup> is the ONLY way to write Vue 3.5+ components
|
|
22
|
+
// It compiles to a render function with zero boilerplate
|
|
23
|
+
|
|
24
|
+
import { ref, computed, onMounted } from "vue";
|
|
25
|
+
import { useRouter } from "vue-router";
|
|
26
|
+
|
|
27
|
+
// Props
|
|
28
|
+
const props = defineProps<{
|
|
29
|
+
title: string;
|
|
30
|
+
count?: number;
|
|
31
|
+
items: string[];
|
|
32
|
+
}>();
|
|
33
|
+
|
|
34
|
+
// Props with defaults
|
|
35
|
+
const props = withDefaults(defineProps<{
|
|
36
|
+
title: string;
|
|
37
|
+
variant?: "primary" | "secondary";
|
|
38
|
+
size?: number;
|
|
39
|
+
}>(), {
|
|
40
|
+
variant: "primary",
|
|
41
|
+
size: 16,
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
// Emits (typed)
|
|
45
|
+
const emit = defineEmits<{
|
|
46
|
+
update: [value: string];
|
|
47
|
+
delete: [id: number];
|
|
48
|
+
"item-click": [item: Item, index: number];
|
|
49
|
+
}>();
|
|
50
|
+
|
|
51
|
+
// Expose (for parent ref access)
|
|
52
|
+
defineExpose({
|
|
53
|
+
reset: () => { /* ... */ },
|
|
54
|
+
focus: () => { /* ... */ },
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
// Models (Vue 3.4+ — replaces v-model boilerplate)
|
|
58
|
+
const modelValue = defineModel<string>(); // default v-model
|
|
59
|
+
const count = defineModel<number>("count"); // named v-model
|
|
60
|
+
|
|
61
|
+
// ❌ HALLUCINATION TRAP: defineModel was added in Vue 3.4+
|
|
62
|
+
// Before 3.4, v-model required manual prop + emit boilerplate
|
|
63
|
+
// ❌ HALLUCINATION TRAP: Do NOT use defineComponent() with <script setup>
|
|
64
|
+
// <script setup> IS the setup function — defineComponent is redundant
|
|
65
|
+
</script>
|
|
66
|
+
```
|
|
67
|
+
|
|
68
|
+
### Options API vs Composition API
|
|
69
|
+
|
|
70
|
+
```vue
|
|
71
|
+
<!-- ❌ LEGACY — Options API (Vue 2 pattern) -->
|
|
72
|
+
<script>
|
|
73
|
+
export default {
|
|
74
|
+
data() { return { count: 0 } },
|
|
75
|
+
computed: { doubled() { return this.count * 2 } },
|
|
76
|
+
methods: { increment() { this.count++ } },
|
|
77
|
+
mounted() { console.log("mounted") },
|
|
78
|
+
}
|
|
79
|
+
</script>
|
|
80
|
+
|
|
81
|
+
<!-- ✅ MODERN — Composition API with <script setup> -->
|
|
82
|
+
<script setup lang="ts">
|
|
83
|
+
import { ref, computed, onMounted } from "vue";
|
|
84
|
+
|
|
85
|
+
const count = ref(0);
|
|
86
|
+
const doubled = computed(() => count.value * 2);
|
|
87
|
+
function increment() { count.value++; }
|
|
88
|
+
onMounted(() => console.log("mounted"));
|
|
89
|
+
</script>
|
|
90
|
+
|
|
91
|
+
<!-- ❌ HALLUCINATION TRAP: Never generate Options API in Vue 3.5+ projects
|
|
92
|
+
unless explicitly maintaining legacy code -->
|
|
93
|
+
```
|
|
94
|
+
|
|
95
|
+
---
|
|
96
|
+
|
|
97
|
+
## Reactivity System
|
|
98
|
+
|
|
99
|
+
### ref vs reactive
|
|
100
|
+
|
|
101
|
+
```ts
|
|
102
|
+
import { ref, reactive, toRefs, toRef } from "vue";
|
|
103
|
+
|
|
104
|
+
// ref — single value (primitive or object)
|
|
105
|
+
const count = ref(0);
|
|
106
|
+
count.value++; // must use .value in <script>
|
|
107
|
+
// In <template>, .value is auto-unwrapped: {{ count }}
|
|
108
|
+
|
|
109
|
+
// reactive — object (deep reactive)
|
|
110
|
+
const state = reactive({
|
|
111
|
+
user: { name: "Alice", age: 30 },
|
|
112
|
+
items: ["a", "b", "c"],
|
|
113
|
+
});
|
|
114
|
+
state.user.name = "Bob"; // direct mutation (no .value needed)
|
|
115
|
+
|
|
116
|
+
// ❌ HALLUCINATION TRAP: Destructuring reactive LOSES reactivity
|
|
117
|
+
const { name, age } = state.user; // ❌ name and age are NOT reactive
|
|
118
|
+
|
|
119
|
+
// ✅ Fix: use toRefs
|
|
120
|
+
const { name, age } = toRefs(state.user); // name.value and age.value ARE reactive
|
|
121
|
+
|
|
122
|
+
// ✅ Fix: use toRef for a single property
|
|
123
|
+
const name = toRef(state.user, "name");
|
|
124
|
+
|
|
125
|
+
// RULE: Prefer ref() for everything. Use reactive() only for complex nested state.
|
|
126
|
+
// ref() is more predictable — .value makes reactivity explicit.
|
|
127
|
+
```
|
|
128
|
+
|
|
129
|
+
### Computed
|
|
130
|
+
|
|
131
|
+
```ts
|
|
132
|
+
import { ref, computed } from "vue";
|
|
133
|
+
|
|
134
|
+
const items = ref<Item[]>([]);
|
|
135
|
+
const searchQuery = ref("");
|
|
136
|
+
|
|
137
|
+
// Read-only computed
|
|
138
|
+
const filteredItems = computed(() =>
|
|
139
|
+
items.value.filter((item) =>
|
|
140
|
+
item.name.toLowerCase().includes(searchQuery.value.toLowerCase())
|
|
141
|
+
)
|
|
142
|
+
);
|
|
143
|
+
|
|
144
|
+
// Writable computed
|
|
145
|
+
const fullName = computed({
|
|
146
|
+
get: () => `${firstName.value} ${lastName.value}`,
|
|
147
|
+
set: (val: string) => {
|
|
148
|
+
const [first, ...rest] = val.split(" ");
|
|
149
|
+
firstName.value = first;
|
|
150
|
+
lastName.value = rest.join(" ");
|
|
151
|
+
},
|
|
152
|
+
});
|
|
153
|
+
```
|
|
154
|
+
|
|
155
|
+
### Watchers
|
|
156
|
+
|
|
157
|
+
```ts
|
|
158
|
+
import { ref, watch, watchEffect, watchPostEffect } from "vue";
|
|
159
|
+
|
|
160
|
+
const query = ref("");
|
|
161
|
+
const userId = ref(1);
|
|
162
|
+
|
|
163
|
+
// watch — explicit sources, previous value available
|
|
164
|
+
watch(query, (newVal, oldVal) => {
|
|
165
|
+
console.log(`Query changed: ${oldVal} → ${newVal}`);
|
|
166
|
+
fetchResults(newVal);
|
|
167
|
+
});
|
|
168
|
+
|
|
169
|
+
// Watch multiple sources
|
|
170
|
+
watch([query, userId], ([newQuery, newId], [oldQuery, oldId]) => {
|
|
171
|
+
console.log("Query or userId changed");
|
|
172
|
+
});
|
|
173
|
+
|
|
174
|
+
// Deep watch (for objects/arrays)
|
|
175
|
+
watch(
|
|
176
|
+
() => state.user,
|
|
177
|
+
(newUser) => { console.log("User changed:", newUser); },
|
|
178
|
+
{ deep: true }
|
|
179
|
+
);
|
|
180
|
+
|
|
181
|
+
// Immediate watch (runs on mount)
|
|
182
|
+
watch(userId, (id) => fetchUser(id), { immediate: true });
|
|
183
|
+
|
|
184
|
+
// watchEffect — auto-tracks dependencies
|
|
185
|
+
watchEffect(async () => {
|
|
186
|
+
// Automatically re-runs when query.value or userId.value changes
|
|
187
|
+
const data = await fetch(`/api/search?q=${query.value}&user=${userId.value}`);
|
|
188
|
+
results.value = await data.json();
|
|
189
|
+
});
|
|
190
|
+
|
|
191
|
+
// watchPostEffect — runs after DOM update (replaces watchEffect with flush: 'post')
|
|
192
|
+
watchPostEffect(() => {
|
|
193
|
+
// Safe to access updated DOM here
|
|
194
|
+
scrollToBottom(container.value);
|
|
195
|
+
});
|
|
196
|
+
|
|
197
|
+
// Cleanup (prevent stale async results)
|
|
198
|
+
watchEffect((onCleanup) => {
|
|
199
|
+
const controller = new AbortController();
|
|
200
|
+
onCleanup(() => controller.abort());
|
|
201
|
+
|
|
202
|
+
fetch(`/api/data?q=${query.value}`, { signal: controller.signal })
|
|
203
|
+
.then((res) => res.json())
|
|
204
|
+
.then((data) => { results.value = data; });
|
|
205
|
+
});
|
|
206
|
+
```
|
|
207
|
+
|
|
208
|
+
---
|
|
209
|
+
|
|
210
|
+
## Composables (Custom Hooks)
|
|
211
|
+
|
|
212
|
+
```ts
|
|
213
|
+
// composables/useFetch.ts
|
|
214
|
+
import { ref, watchEffect, type Ref } from "vue";
|
|
215
|
+
|
|
216
|
+
interface UseFetchReturn<T> {
|
|
217
|
+
data: Ref<T | null>;
|
|
218
|
+
error: Ref<Error | null>;
|
|
219
|
+
isLoading: Ref<boolean>;
|
|
220
|
+
refresh: () => Promise<void>;
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
export function useFetch<T>(url: Ref<string> | string): UseFetchReturn<T> {
|
|
224
|
+
const data = ref<T | null>(null) as Ref<T | null>;
|
|
225
|
+
const error = ref<Error | null>(null);
|
|
226
|
+
const isLoading = ref(false);
|
|
227
|
+
|
|
228
|
+
async function fetchData() {
|
|
229
|
+
isLoading.value = true;
|
|
230
|
+
error.value = null;
|
|
231
|
+
|
|
232
|
+
try {
|
|
233
|
+
const resolvedUrl = typeof url === "string" ? url : url.value;
|
|
234
|
+
const response = await fetch(resolvedUrl);
|
|
235
|
+
if (!response.ok) throw new Error(`HTTP ${response.status}`);
|
|
236
|
+
data.value = await response.json();
|
|
237
|
+
} catch (e) {
|
|
238
|
+
error.value = e instanceof Error ? e : new Error(String(e));
|
|
239
|
+
} finally {
|
|
240
|
+
isLoading.value = false;
|
|
241
|
+
}
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
// Auto-refetch when URL changes (if URL is a ref)
|
|
245
|
+
if (typeof url !== "string") {
|
|
246
|
+
watchEffect(() => {
|
|
247
|
+
if (url.value) fetchData();
|
|
248
|
+
});
|
|
249
|
+
} else {
|
|
250
|
+
fetchData();
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
return { data, error, isLoading, refresh: fetchData };
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
// Usage:
|
|
257
|
+
const apiUrl = computed(() => `/api/users/${userId.value}`);
|
|
258
|
+
const { data: user, isLoading, error } = useFetch<User>(apiUrl);
|
|
259
|
+
```
|
|
260
|
+
|
|
261
|
+
### useLocalStorage Composable
|
|
262
|
+
|
|
263
|
+
```ts
|
|
264
|
+
// composables/useLocalStorage.ts
|
|
265
|
+
import { ref, watch, type Ref } from "vue";
|
|
266
|
+
|
|
267
|
+
export function useLocalStorage<T>(key: string, defaultValue: T): Ref<T> {
|
|
268
|
+
const stored = localStorage.getItem(key);
|
|
269
|
+
const data = ref<T>(stored ? JSON.parse(stored) : defaultValue) as Ref<T>;
|
|
270
|
+
|
|
271
|
+
watch(data, (newVal) => {
|
|
272
|
+
localStorage.setItem(key, JSON.stringify(newVal));
|
|
273
|
+
}, { deep: true });
|
|
274
|
+
|
|
275
|
+
return data;
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
// Usage:
|
|
279
|
+
const theme = useLocalStorage("theme", "dark");
|
|
280
|
+
theme.value = "light"; // auto-saves to localStorage
|
|
281
|
+
```
|
|
282
|
+
|
|
283
|
+
### useDebounce Composable
|
|
284
|
+
|
|
285
|
+
```ts
|
|
286
|
+
// composables/useDebounce.ts
|
|
287
|
+
import { ref, watch, type Ref } from "vue";
|
|
288
|
+
|
|
289
|
+
export function useDebounce<T>(source: Ref<T>, delay = 300): Ref<T> {
|
|
290
|
+
const debounced = ref(source.value) as Ref<T>;
|
|
291
|
+
|
|
292
|
+
let timeout: ReturnType<typeof setTimeout>;
|
|
293
|
+
watch(source, (val) => {
|
|
294
|
+
clearTimeout(timeout);
|
|
295
|
+
timeout = setTimeout(() => { debounced.value = val; }, delay);
|
|
296
|
+
});
|
|
297
|
+
|
|
298
|
+
return debounced;
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
// Usage:
|
|
302
|
+
const query = ref("");
|
|
303
|
+
const debouncedQuery = useDebounce(query, 500);
|
|
304
|
+
// debouncedQuery only updates 500ms after the user stops typing
|
|
305
|
+
```
|
|
306
|
+
|
|
307
|
+
---
|
|
308
|
+
|
|
309
|
+
## Pinia (State Management)
|
|
310
|
+
|
|
311
|
+
### Setup Store (Recommended)
|
|
312
|
+
|
|
313
|
+
```ts
|
|
314
|
+
// stores/cart.ts
|
|
315
|
+
import { defineStore } from "pinia";
|
|
316
|
+
import { ref, computed } from "vue";
|
|
317
|
+
|
|
318
|
+
export const useCartStore = defineStore("cart", () => {
|
|
319
|
+
// State
|
|
320
|
+
const items = ref<CartItem[]>([]);
|
|
321
|
+
|
|
322
|
+
// Getters (computed)
|
|
323
|
+
const totalItems = computed(() => items.value.length);
|
|
324
|
+
const totalPrice = computed(() =>
|
|
325
|
+
items.value.reduce((sum, item) => sum + item.price * item.quantity, 0)
|
|
326
|
+
);
|
|
327
|
+
const isEmpty = computed(() => items.value.length === 0);
|
|
328
|
+
|
|
329
|
+
// Actions
|
|
330
|
+
function addItem(product: Product, quantity = 1) {
|
|
331
|
+
const existing = items.value.find((i) => i.productId === product.id);
|
|
332
|
+
if (existing) {
|
|
333
|
+
existing.quantity += quantity;
|
|
334
|
+
} else {
|
|
335
|
+
items.value.push({
|
|
336
|
+
productId: product.id,
|
|
337
|
+
name: product.name,
|
|
338
|
+
price: product.price,
|
|
339
|
+
quantity,
|
|
340
|
+
});
|
|
341
|
+
}
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
function removeItem(productId: string) {
|
|
345
|
+
items.value = items.value.filter((i) => i.productId !== productId);
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
function clearCart() {
|
|
349
|
+
items.value = [];
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
// Async action
|
|
353
|
+
async function checkout() {
|
|
354
|
+
const response = await fetch("/api/checkout", {
|
|
355
|
+
method: "POST",
|
|
356
|
+
body: JSON.stringify({ items: items.value }),
|
|
357
|
+
});
|
|
358
|
+
if (response.ok) clearCart();
|
|
359
|
+
return response.json();
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
return { items, totalItems, totalPrice, isEmpty, addItem, removeItem, clearCart, checkout };
|
|
363
|
+
});
|
|
364
|
+
|
|
365
|
+
// ❌ HALLUCINATION TRAP: Do NOT use Vuex in Vue 3.5+ projects
|
|
366
|
+
// Vuex is in maintenance mode. Pinia is the official replacement.
|
|
367
|
+
// ❌ import { createStore } from "vuex" ← LEGACY
|
|
368
|
+
// ✅ import { defineStore } from "pinia"
|
|
369
|
+
```
|
|
370
|
+
|
|
371
|
+
### Using Stores in Components
|
|
372
|
+
|
|
373
|
+
```vue
|
|
374
|
+
<script setup lang="ts">
|
|
375
|
+
import { useCartStore } from "@/stores/cart";
|
|
376
|
+
import { storeToRefs } from "pinia";
|
|
377
|
+
|
|
378
|
+
const cartStore = useCartStore();
|
|
379
|
+
|
|
380
|
+
// ✅ Use storeToRefs for reactive destructuring of state/getters
|
|
381
|
+
const { items, totalPrice, isEmpty } = storeToRefs(cartStore);
|
|
382
|
+
|
|
383
|
+
// Actions can be destructured directly (they're not reactive)
|
|
384
|
+
const { addItem, removeItem, clearCart } = cartStore;
|
|
385
|
+
|
|
386
|
+
// ❌ HALLUCINATION TRAP: Destructuring state WITHOUT storeToRefs loses reactivity
|
|
387
|
+
// ❌ const { items, totalPrice } = cartStore; ← NOT reactive!
|
|
388
|
+
// ✅ const { items, totalPrice } = storeToRefs(cartStore); ← reactive
|
|
389
|
+
</script>
|
|
390
|
+
|
|
391
|
+
<template>
|
|
392
|
+
<div v-if="isEmpty">Cart is empty</div>
|
|
393
|
+
<ul v-else>
|
|
394
|
+
<li v-for="item in items" :key="item.productId">
|
|
395
|
+
{{ item.name }} — ${{ item.price }} × {{ item.quantity }}
|
|
396
|
+
<button @click="removeItem(item.productId)">Remove</button>
|
|
397
|
+
</li>
|
|
398
|
+
</ul>
|
|
399
|
+
<p>Total: ${{ totalPrice.toFixed(2) }}</p>
|
|
400
|
+
</template>
|
|
401
|
+
```
|
|
402
|
+
|
|
403
|
+
### Pinia Persistence Plugin
|
|
404
|
+
|
|
405
|
+
```ts
|
|
406
|
+
// main.ts
|
|
407
|
+
import { createPinia } from "pinia";
|
|
408
|
+
import piniaPluginPersistedstate from "pinia-plugin-persistedstate";
|
|
409
|
+
|
|
410
|
+
const pinia = createPinia();
|
|
411
|
+
pinia.use(piniaPluginPersistedstate);
|
|
412
|
+
|
|
413
|
+
// In store:
|
|
414
|
+
export const useSettingsStore = defineStore("settings", () => {
|
|
415
|
+
const theme = ref("dark");
|
|
416
|
+
const locale = ref("en");
|
|
417
|
+
return { theme, locale };
|
|
418
|
+
}, {
|
|
419
|
+
persist: true, // auto-saves to localStorage
|
|
420
|
+
});
|
|
421
|
+
```
|
|
422
|
+
|
|
423
|
+
---
|
|
424
|
+
|
|
425
|
+
## Vue Router 4
|
|
426
|
+
|
|
427
|
+
### Route Configuration
|
|
428
|
+
|
|
429
|
+
```ts
|
|
430
|
+
// router/index.ts
|
|
431
|
+
import { createRouter, createWebHistory, type RouteRecordRaw } from "vue-router";
|
|
432
|
+
|
|
433
|
+
const routes: RouteRecordRaw[] = [
|
|
434
|
+
{
|
|
435
|
+
path: "/",
|
|
436
|
+
component: () => import("@/layouts/DefaultLayout.vue"),
|
|
437
|
+
children: [
|
|
438
|
+
{ path: "", name: "home", component: () => import("@/pages/Home.vue") },
|
|
439
|
+
{ path: "about", name: "about", component: () => import("@/pages/About.vue") },
|
|
440
|
+
],
|
|
441
|
+
},
|
|
442
|
+
{
|
|
443
|
+
path: "/dashboard",
|
|
444
|
+
component: () => import("@/layouts/DashboardLayout.vue"),
|
|
445
|
+
meta: { requiresAuth: true },
|
|
446
|
+
children: [
|
|
447
|
+
{ path: "", name: "dashboard", component: () => import("@/pages/Dashboard.vue") },
|
|
448
|
+
{
|
|
449
|
+
path: "users/:id",
|
|
450
|
+
name: "user-detail",
|
|
451
|
+
component: () => import("@/pages/UserDetail.vue"),
|
|
452
|
+
props: true, // pass route params as props
|
|
453
|
+
},
|
|
454
|
+
],
|
|
455
|
+
},
|
|
456
|
+
{ path: "/:pathMatch(.*)*", name: "not-found", component: () => import("@/pages/NotFound.vue") },
|
|
457
|
+
];
|
|
458
|
+
|
|
459
|
+
const router = createRouter({
|
|
460
|
+
history: createWebHistory(),
|
|
461
|
+
routes,
|
|
462
|
+
scrollBehavior(to, from, savedPosition) {
|
|
463
|
+
return savedPosition || { top: 0 };
|
|
464
|
+
},
|
|
465
|
+
});
|
|
466
|
+
|
|
467
|
+
// Navigation guard
|
|
468
|
+
router.beforeEach(async (to, from) => {
|
|
469
|
+
const authStore = useAuthStore();
|
|
470
|
+
|
|
471
|
+
if (to.meta.requiresAuth && !authStore.isAuthenticated) {
|
|
472
|
+
return { name: "login", query: { redirect: to.fullPath } };
|
|
473
|
+
}
|
|
474
|
+
});
|
|
475
|
+
|
|
476
|
+
export default router;
|
|
477
|
+
```
|
|
478
|
+
|
|
479
|
+
### Router Composables
|
|
480
|
+
|
|
481
|
+
```vue
|
|
482
|
+
<script setup lang="ts">
|
|
483
|
+
import { useRouter, useRoute } from "vue-router";
|
|
484
|
+
|
|
485
|
+
const router = useRouter();
|
|
486
|
+
const route = useRoute();
|
|
487
|
+
|
|
488
|
+
// Reactive route params
|
|
489
|
+
const userId = computed(() => route.params.id as string);
|
|
490
|
+
|
|
491
|
+
// Programmatic navigation
|
|
492
|
+
function goToUser(id: string) {
|
|
493
|
+
router.push({ name: "user-detail", params: { id } });
|
|
494
|
+
}
|
|
495
|
+
|
|
496
|
+
function goBack() {
|
|
497
|
+
router.back();
|
|
498
|
+
}
|
|
499
|
+
|
|
500
|
+
// ❌ HALLUCINATION TRAP: route.params values are always strings
|
|
501
|
+
// Even if the URL is /users/123, params.id is "123" (string), not 123 (number)
|
|
502
|
+
// Always parse: parseInt(route.params.id as string)
|
|
503
|
+
</script>
|
|
504
|
+
```
|
|
505
|
+
|
|
506
|
+
---
|
|
507
|
+
|
|
508
|
+
## Component Patterns
|
|
509
|
+
|
|
510
|
+
### Slots
|
|
511
|
+
|
|
512
|
+
```vue
|
|
513
|
+
<!-- BaseCard.vue -->
|
|
514
|
+
<template>
|
|
515
|
+
<div class="card">
|
|
516
|
+
<header v-if="$slots.header" class="card-header">
|
|
517
|
+
<slot name="header" />
|
|
518
|
+
</header>
|
|
519
|
+
|
|
520
|
+
<div class="card-body">
|
|
521
|
+
<slot /> <!-- default slot -->
|
|
522
|
+
</div>
|
|
523
|
+
|
|
524
|
+
<footer v-if="$slots.footer" class="card-footer">
|
|
525
|
+
<slot name="footer" />
|
|
526
|
+
</footer>
|
|
527
|
+
</div>
|
|
528
|
+
</template>
|
|
529
|
+
|
|
530
|
+
<!-- Scoped slot (pass data to parent) -->
|
|
531
|
+
<!-- DataList.vue -->
|
|
532
|
+
<template>
|
|
533
|
+
<ul>
|
|
534
|
+
<li v-for="(item, index) in items" :key="item.id">
|
|
535
|
+
<slot name="item" :item="item" :index="index" :is-last="index === items.length - 1">
|
|
536
|
+
<!-- Default content if parent doesn't provide slot -->
|
|
537
|
+
{{ item.name }}
|
|
538
|
+
</slot>
|
|
539
|
+
</li>
|
|
540
|
+
</ul>
|
|
541
|
+
</template>
|
|
542
|
+
|
|
543
|
+
<!-- Usage with scoped slot -->
|
|
544
|
+
<DataList :items="users">
|
|
545
|
+
<template #item="{ item, index, isLast }">
|
|
546
|
+
<UserCard :user="item" :highlighted="index === 0" />
|
|
547
|
+
<hr v-if="!isLast" />
|
|
548
|
+
</template>
|
|
549
|
+
</DataList>
|
|
550
|
+
```
|
|
551
|
+
|
|
552
|
+
### Provide / Inject (Dependency Injection)
|
|
553
|
+
|
|
554
|
+
```ts
|
|
555
|
+
// Parent component
|
|
556
|
+
import { provide, ref, type InjectionKey } from "vue";
|
|
557
|
+
|
|
558
|
+
interface ThemeContext {
|
|
559
|
+
theme: Ref<string>;
|
|
560
|
+
toggleTheme: () => void;
|
|
561
|
+
}
|
|
562
|
+
|
|
563
|
+
export const ThemeKey: InjectionKey<ThemeContext> = Symbol("theme");
|
|
564
|
+
|
|
565
|
+
// In parent <script setup>:
|
|
566
|
+
const theme = ref("dark");
|
|
567
|
+
function toggleTheme() {
|
|
568
|
+
theme.value = theme.value === "dark" ? "light" : "dark";
|
|
569
|
+
}
|
|
570
|
+
provide(ThemeKey, { theme, toggleTheme });
|
|
571
|
+
|
|
572
|
+
// Child component (any depth)
|
|
573
|
+
import { inject } from "vue";
|
|
574
|
+
import { ThemeKey } from "@/keys";
|
|
575
|
+
|
|
576
|
+
const themeCtx = inject(ThemeKey);
|
|
577
|
+
if (!themeCtx) throw new Error("ThemeKey not provided");
|
|
578
|
+
// themeCtx.theme.value === "dark"
|
|
579
|
+
|
|
580
|
+
// ❌ HALLUCINATION TRAP: Always use InjectionKey<T> for type safety
|
|
581
|
+
// inject("theme") returns unknown — inject(ThemeKey) returns ThemeContext
|
|
582
|
+
```
|
|
583
|
+
|
|
584
|
+
### Teleport
|
|
585
|
+
|
|
586
|
+
```vue
|
|
587
|
+
<!-- Render modal content at <body> level to escape overflow/z-index traps -->
|
|
588
|
+
<Teleport to="body">
|
|
589
|
+
<div v-if="showModal" class="modal-overlay">
|
|
590
|
+
<div class="modal-content">
|
|
591
|
+
<slot />
|
|
592
|
+
<button @click="$emit('close')">Close</button>
|
|
593
|
+
</div>
|
|
594
|
+
</div>
|
|
595
|
+
</Teleport>
|
|
596
|
+
```
|
|
597
|
+
|
|
598
|
+
### Transition
|
|
599
|
+
|
|
600
|
+
```vue
|
|
601
|
+
<Transition
|
|
602
|
+
name="fade"
|
|
603
|
+
mode="out-in"
|
|
604
|
+
@before-enter="onBeforeEnter"
|
|
605
|
+
@enter="onEnter"
|
|
606
|
+
@leave="onLeave"
|
|
607
|
+
>
|
|
608
|
+
<component :is="currentComponent" :key="currentRoute" />
|
|
609
|
+
</Transition>
|
|
610
|
+
|
|
611
|
+
<style>
|
|
612
|
+
.fade-enter-active, .fade-leave-active {
|
|
613
|
+
transition: opacity 0.3s ease;
|
|
614
|
+
}
|
|
615
|
+
.fade-enter-from, .fade-leave-to {
|
|
616
|
+
opacity: 0;
|
|
617
|
+
}
|
|
618
|
+
</style>
|
|
619
|
+
|
|
620
|
+
<!-- TransitionGroup for lists -->
|
|
621
|
+
<TransitionGroup name="list" tag="ul">
|
|
622
|
+
<li v-for="item in items" :key="item.id">
|
|
623
|
+
{{ item.name }}
|
|
624
|
+
</li>
|
|
625
|
+
</TransitionGroup>
|
|
626
|
+
|
|
627
|
+
<style>
|
|
628
|
+
.list-enter-active, .list-leave-active {
|
|
629
|
+
transition: all 0.3s ease;
|
|
630
|
+
}
|
|
631
|
+
.list-enter-from, .list-leave-from {
|
|
632
|
+
opacity: 0;
|
|
633
|
+
transform: translateY(20px);
|
|
634
|
+
}
|
|
635
|
+
.list-leave-active {
|
|
636
|
+
position: absolute; /* prevents layout shift during leave */
|
|
637
|
+
}
|
|
638
|
+
</style>
|
|
639
|
+
```
|
|
640
|
+
|
|
641
|
+
---
|
|
642
|
+
|
|
643
|
+
## Nuxt 4
|
|
644
|
+
|
|
645
|
+
### File-Based Routing
|
|
646
|
+
|
|
647
|
+
```
|
|
648
|
+
pages/
|
|
649
|
+
├── index.vue → /
|
|
650
|
+
├── about.vue → /about
|
|
651
|
+
├── users/
|
|
652
|
+
│ ├── index.vue → /users
|
|
653
|
+
│ └── [id].vue → /users/:id
|
|
654
|
+
├── blog/
|
|
655
|
+
│ └── [...slug].vue → /blog/* (catch-all)
|
|
656
|
+
└── [[optional]].vue → /:optional? (optional param)
|
|
657
|
+
```
|
|
658
|
+
|
|
659
|
+
### Data Fetching
|
|
660
|
+
|
|
661
|
+
```vue
|
|
662
|
+
<script setup lang="ts">
|
|
663
|
+
// useFetch — SSR-friendly, auto-cached, deduped
|
|
664
|
+
const { data: users, status, error, refresh } = await useFetch<User[]>("/api/users", {
|
|
665
|
+
query: { page: currentPage }, // reactive query params
|
|
666
|
+
pick: ["id", "name", "email"], // only extract these fields (reduces payload)
|
|
667
|
+
transform: (data) => data.filter((u) => u.isActive), // client-side transform
|
|
668
|
+
watch: [currentPage], // auto-refetch when page changes
|
|
669
|
+
});
|
|
670
|
+
|
|
671
|
+
// useAsyncData — for non-fetch async operations
|
|
672
|
+
const { data: config } = await useAsyncData("app-config", () => {
|
|
673
|
+
return $fetch("/api/config");
|
|
674
|
+
});
|
|
675
|
+
|
|
676
|
+
// $fetch — raw fetch (NOT SSR-cached, no dedup)
|
|
677
|
+
const data = await $fetch("/api/endpoint");
|
|
678
|
+
|
|
679
|
+
// ❌ HALLUCINATION TRAP: useFetch auto-deduplicates during SSR
|
|
680
|
+
// Calling useFetch twice with the same key returns the same promise
|
|
681
|
+
// Use $fetch when you intentionally want separate requests
|
|
682
|
+
|
|
683
|
+
// ❌ HALLUCINATION TRAP: useFetch MUST be called at the top level of setup
|
|
684
|
+
// It cannot be called inside functions, loops, or conditionals
|
|
685
|
+
</script>
|
|
686
|
+
|
|
687
|
+
<template>
|
|
688
|
+
<div v-if="status === 'pending'">Loading...</div>
|
|
689
|
+
<div v-else-if="error">Error: {{ error.message }}</div>
|
|
690
|
+
<ul v-else>
|
|
691
|
+
<li v-for="user in users" :key="user.id">{{ user.name }}</li>
|
|
692
|
+
</ul>
|
|
693
|
+
</template>
|
|
694
|
+
```
|
|
695
|
+
|
|
696
|
+
### Runtime Config
|
|
697
|
+
|
|
698
|
+
```ts
|
|
699
|
+
// nuxt.config.ts
|
|
700
|
+
export default defineNuxtConfig({
|
|
701
|
+
runtimeConfig: {
|
|
702
|
+
// Server-only (never exposed to client)
|
|
703
|
+
apiSecret: process.env.API_SECRET,
|
|
704
|
+
dbUrl: process.env.DATABASE_URL,
|
|
705
|
+
|
|
706
|
+
// Client-accessible
|
|
707
|
+
public: {
|
|
708
|
+
apiBase: process.env.NUXT_PUBLIC_API_BASE || "https://api.example.com",
|
|
709
|
+
appName: "My App",
|
|
710
|
+
},
|
|
711
|
+
},
|
|
712
|
+
});
|
|
713
|
+
|
|
714
|
+
// Usage in components/composables:
|
|
715
|
+
const config = useRuntimeConfig();
|
|
716
|
+
// config.public.apiBase — ✅ accessible on client and server
|
|
717
|
+
// config.apiSecret — ✅ accessible on server only, undefined on client
|
|
718
|
+
|
|
719
|
+
// ❌ HALLUCINATION TRAP: Private keys are ONLY available server-side
|
|
720
|
+
// Accessing config.apiSecret in a client component returns undefined
|
|
721
|
+
```
|
|
722
|
+
|
|
723
|
+
### Server Routes (Nitro)
|
|
724
|
+
|
|
725
|
+
```ts
|
|
726
|
+
// server/api/users.get.ts — responds to GET /api/users
|
|
727
|
+
export default defineEventHandler(async (event) => {
|
|
728
|
+
const query = getQuery(event);
|
|
729
|
+
const users = await db.user.findMany({
|
|
730
|
+
skip: Number(query.offset) || 0,
|
|
731
|
+
take: Number(query.limit) || 20,
|
|
732
|
+
});
|
|
733
|
+
return users;
|
|
734
|
+
});
|
|
735
|
+
|
|
736
|
+
// server/api/users.post.ts — responds to POST /api/users
|
|
737
|
+
export default defineEventHandler(async (event) => {
|
|
738
|
+
const body = await readBody(event);
|
|
739
|
+
// Validate body...
|
|
740
|
+
const user = await db.user.create({ data: body });
|
|
741
|
+
return user;
|
|
742
|
+
});
|
|
743
|
+
|
|
744
|
+
// server/api/users/[id].get.ts — responds to GET /api/users/:id
|
|
745
|
+
export default defineEventHandler(async (event) => {
|
|
746
|
+
const id = getRouterParam(event, "id");
|
|
747
|
+
const user = await db.user.findUnique({ where: { id } });
|
|
748
|
+
if (!user) {
|
|
749
|
+
throw createError({ statusCode: 404, message: "User not found" });
|
|
750
|
+
}
|
|
751
|
+
return user;
|
|
752
|
+
});
|
|
753
|
+
```
|
|
754
|
+
|
|
755
|
+
---
|
|
756
|
+
|
|
757
|
+
## TypeScript Integration
|
|
758
|
+
|
|
759
|
+
```vue
|
|
760
|
+
<script setup lang="ts">
|
|
761
|
+
// Component with full TypeScript
|
|
762
|
+
interface User {
|
|
763
|
+
id: number;
|
|
764
|
+
name: string;
|
|
765
|
+
email: string;
|
|
766
|
+
role: "admin" | "user" | "moderator";
|
|
767
|
+
}
|
|
768
|
+
|
|
769
|
+
const props = defineProps<{
|
|
770
|
+
user: User;
|
|
771
|
+
editable?: boolean;
|
|
772
|
+
}>();
|
|
773
|
+
|
|
774
|
+
const emit = defineEmits<{
|
|
775
|
+
save: [user: User];
|
|
776
|
+
cancel: [];
|
|
777
|
+
}>();
|
|
778
|
+
|
|
779
|
+
// Template refs
|
|
780
|
+
const inputRef = ref<HTMLInputElement | null>(null);
|
|
781
|
+
const formRef = ref<InstanceType<typeof FormComponent> | null>(null);
|
|
782
|
+
|
|
783
|
+
onMounted(() => {
|
|
784
|
+
inputRef.value?.focus();
|
|
785
|
+
});
|
|
786
|
+
</script>
|
|
787
|
+
```
|
|
788
|
+
|
|
789
|
+
---
|
|
790
|
+
|
|
791
|
+
## Performance Optimization
|
|
792
|
+
|
|
793
|
+
```vue
|
|
794
|
+
<script setup lang="ts">
|
|
795
|
+
import { shallowRef, shallowReactive, triggerRef } from "vue";
|
|
796
|
+
|
|
797
|
+
// shallowRef — only tracks .value changes, not deep mutations
|
|
798
|
+
const bigList = shallowRef<Item[]>([]);
|
|
799
|
+
// Mutating items inside won't trigger updates
|
|
800
|
+
// Must replace the entire array: bigList.value = [...newItems]
|
|
801
|
+
// Or manually trigger: triggerRef(bigList)
|
|
802
|
+
|
|
803
|
+
// v-once — render once, never update (static content)
|
|
804
|
+
// v-memo — skip re-rendering unless dependencies change
|
|
805
|
+
</script>
|
|
806
|
+
|
|
807
|
+
<template>
|
|
808
|
+
<!-- Static content — rendered once -->
|
|
809
|
+
<footer v-once>
|
|
810
|
+
<p>© 2024 My Company. All rights reserved.</p>
|
|
811
|
+
</footer>
|
|
812
|
+
|
|
813
|
+
<!-- v-memo — skip re-render unless item.id or selected changes -->
|
|
814
|
+
<div v-for="item in list" :key="item.id" v-memo="[item.id, selected === item.id]">
|
|
815
|
+
<ItemCard :item="item" :selected="selected === item.id" />
|
|
816
|
+
</div>
|
|
817
|
+
|
|
818
|
+
<!-- Async components (lazy loading) -->
|
|
819
|
+
<component :is="defineAsyncComponent(() => import('./HeavyWidget.vue'))" />
|
|
820
|
+
</template>
|
|
821
|
+
```
|
|
822
|
+
|
|
823
|
+
---
|
|
824
|
+
|
|
825
|
+
## Testing with Vitest
|
|
826
|
+
|
|
827
|
+
```ts
|
|
828
|
+
// tests/components/Counter.test.ts
|
|
829
|
+
import { describe, it, expect } from "vitest";
|
|
830
|
+
import { mount } from "@vue/test-utils";
|
|
831
|
+
import Counter from "@/components/Counter.vue";
|
|
832
|
+
|
|
833
|
+
describe("Counter", () => {
|
|
834
|
+
it("renders initial count", () => {
|
|
835
|
+
const wrapper = mount(Counter, { props: { initial: 5 } });
|
|
836
|
+
expect(wrapper.text()).toContain("5");
|
|
837
|
+
});
|
|
838
|
+
|
|
839
|
+
it("increments on click", async () => {
|
|
840
|
+
const wrapper = mount(Counter);
|
|
841
|
+
await wrapper.find("button").trigger("click");
|
|
842
|
+
expect(wrapper.text()).toContain("1");
|
|
843
|
+
});
|
|
844
|
+
|
|
845
|
+
it("emits update event", async () => {
|
|
846
|
+
const wrapper = mount(Counter);
|
|
847
|
+
await wrapper.find("button").trigger("click");
|
|
848
|
+
expect(wrapper.emitted("update")).toHaveLength(1);
|
|
849
|
+
expect(wrapper.emitted("update")![0]).toEqual([1]);
|
|
850
|
+
});
|
|
851
|
+
});
|
|
852
|
+
|
|
853
|
+
// Testing composables
|
|
854
|
+
import { useDebounce } from "@/composables/useDebounce";
|
|
855
|
+
|
|
856
|
+
describe("useDebounce", () => {
|
|
857
|
+
it("debounces value updates", async () => {
|
|
858
|
+
vi.useFakeTimers();
|
|
859
|
+
const source = ref("hello");
|
|
860
|
+
const debounced = useDebounce(source, 300);
|
|
861
|
+
|
|
862
|
+
source.value = "world";
|
|
863
|
+
expect(debounced.value).toBe("hello"); // not yet
|
|
864
|
+
|
|
865
|
+
vi.advanceTimersByTime(300);
|
|
866
|
+
expect(debounced.value).toBe("world"); // now updated
|
|
867
|
+
|
|
868
|
+
vi.useRealTimers();
|
|
869
|
+
});
|
|
870
|
+
});
|
|
871
|
+
|
|
872
|
+
// Testing Pinia stores
|
|
873
|
+
import { setActivePinia, createPinia } from "pinia";
|
|
874
|
+
import { useCartStore } from "@/stores/cart";
|
|
875
|
+
|
|
876
|
+
describe("Cart Store", () => {
|
|
877
|
+
beforeEach(() => {
|
|
878
|
+
setActivePinia(createPinia());
|
|
879
|
+
});
|
|
880
|
+
|
|
881
|
+
it("adds items", () => {
|
|
882
|
+
const cart = useCartStore();
|
|
883
|
+
cart.addItem({ id: "1", name: "Widget", price: 10 });
|
|
884
|
+
expect(cart.totalItems).toBe(1);
|
|
885
|
+
expect(cart.totalPrice).toBe(10);
|
|
886
|
+
});
|
|
887
|
+
});
|
|
888
|
+
```
|
|
889
|
+
|
|
890
|
+
---
|
|
891
|
+
|
|
892
|
+
## Output Format
|
|
893
|
+
|
|
894
|
+
When this skill produces or reviews code, structure your output as follows:
|
|
895
|
+
|
|
896
|
+
```
|
|
897
|
+
━━━ Vue Expert Report ━━━━━━━━━━━━━━━━━━━━━━━━
|
|
898
|
+
Skill: Vue Expert
|
|
899
|
+
Vue Ver: 3.5+
|
|
900
|
+
Scope: [N files · N components]
|
|
901
|
+
─────────────────────────────────────────────────
|
|
902
|
+
✅ Passed: [checks that passed, or "All clean"]
|
|
903
|
+
⚠️ Warnings: [non-blocking issues, or "None"]
|
|
904
|
+
❌ Blocked: [blocking issues requiring fix, or "None"]
|
|
905
|
+
─────────────────────────────────────────────────
|
|
906
|
+
VBC status: PENDING → VERIFIED
|
|
907
|
+
Evidence: [test output / lint pass / compile success]
|
|
908
|
+
```
|
|
909
|
+
|
|
910
|
+
**VBC (Verification-Before-Completion) is mandatory.**
|
|
911
|
+
Do not mark status as VERIFIED until concrete terminal evidence is provided.
|
|
912
|
+
|
|
913
|
+
---
|
|
914
|
+
|
|
915
|
+
## 🤖 LLM-Specific Traps
|
|
916
|
+
|
|
917
|
+
AI coding assistants often fall into specific bad habits when generating Vue code. These are strictly forbidden:
|
|
918
|
+
|
|
919
|
+
1. **Options API in Vue 3.5+:** Never generate `export default { data(), methods, computed, mounted() }` in modern Vue projects. Always use `<script setup>` with Composition API.
|
|
920
|
+
2. **Vuex Instead of Pinia:** Vuex is in maintenance mode. Never import from `vuex`. Use `pinia` with `defineStore()`.
|
|
921
|
+
3. **Destructuring Reactive Without `toRefs`/`storeToRefs`:** Destructuring a `reactive()` object or a Pinia store without `toRefs()`/`storeToRefs()` breaks reactivity.
|
|
922
|
+
4. **Mutating Props Directly:** Never mutate a prop value. Use `defineEmits` to emit updates, or use `defineModel()` (Vue 3.4+) for v-model bindings.
|
|
923
|
+
5. **Using `v-for` Without `:key`:** Every `v-for` must have a unique, stable `:key` binding. Never use the iteration index as the key if the list can reorder.
|
|
924
|
+
6. **`defineComponent` With `<script setup>`:** `defineComponent()` is redundant inside `<script setup>`. The setup syntax IS the component definition.
|
|
925
|
+
7. **`this` in Composition API:** There is no `this` in `<script setup>`. Access reactive state directly by variable name. `this.count` does not exist.
|
|
926
|
+
8. **`useFetch` Inside Functions:** Nuxt's `useFetch` must be called at the top level of `<script setup>`, not inside callbacks, conditionals, or loops.
|
|
927
|
+
9. **Route Params as Numbers:** `route.params.id` is always a string. Never treat it as a number without explicit parsing (`parseInt()`).
|
|
928
|
+
10. **Exposing Private Runtime Config:** `runtimeConfig` (non-public) keys are server-only. Accessing them in client components returns `undefined`.
|
|
929
|
+
|
|
930
|
+
---
|
|
931
|
+
|
|
932
|
+
## 🏛️ Tribunal Integration (Anti-Hallucination)
|
|
933
|
+
|
|
934
|
+
**Slash command: `/tribunal-frontend`**
|
|
935
|
+
**Active reviewers: `logic` · `security` · `frontend` · `type-safety`**
|
|
936
|
+
|
|
937
|
+
### ❌ Forbidden AI Tropes
|
|
938
|
+
|
|
939
|
+
1. **Blind Assumptions:** Never make an assumption without documenting it clearly with `// VERIFY: [reason]`.
|
|
940
|
+
2. **Silent Degradation:** Catching and suppressing errors without logging or handling.
|
|
941
|
+
3. **Context Amnesia:** Forgetting Vue version, Nuxt vs vanilla Vue, or Pinia vs Vuex constraints.
|
|
942
|
+
4. **Sloppy Layout Generation:** Never build UI without explicit 4px grid spacing and flex/grid layouts.
|
|
943
|
+
|
|
944
|
+
### ✅ Pre-Flight Self-Audit
|
|
945
|
+
|
|
946
|
+
Review these questions before confirming output:
|
|
947
|
+
```
|
|
948
|
+
✅ Did I use <script setup lang="ts"> (not Options API)?
|
|
949
|
+
✅ Did I use Pinia (not Vuex)?
|
|
950
|
+
✅ Did I use storeToRefs for reactive store destructuring?
|
|
951
|
+
✅ Did I use defineModel (Vue 3.4+) for v-model bindings?
|
|
952
|
+
✅ Did I use toRefs when destructuring reactive objects?
|
|
953
|
+
✅ Does every v-for have a unique, stable :key?
|
|
954
|
+
✅ Did I use useRuntimeConfig for env variables (not process.env)?
|
|
955
|
+
✅ Did I call useFetch at the top level of setup?
|
|
956
|
+
✅ Are route params parsed correctly (string → number)?
|
|
957
|
+
✅ Did I write Vitest tests with @vue/test-utils?
|
|
958
|
+
```
|
|
959
|
+
|
|
960
|
+
### 🛑 Verification-Before-Completion (VBC) Protocol
|
|
961
|
+
|
|
962
|
+
**CRITICAL:** You must follow a strict "evidence-based closeout" state machine.
|
|
963
|
+
- ❌ **Forbidden:** Declaring a Vue component "works" because it compiles without errors.
|
|
964
|
+
- ✅ **Required:** You are explicitly forbidden from completing your task without providing **concrete terminal/test evidence** (e.g., passing Vitest logs, successful build, or dev server confirmation) proving the component works correctly.
|