tribunal-kit 3.0.0 → 3.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.
- package/.agent/ARCHITECTURE.md +99 -99
- package/.agent/GEMINI.md +52 -52
- package/.agent/agents/accessibility-reviewer.md +187 -220
- package/.agent/agents/ai-code-reviewer.md +199 -233
- package/.agent/agents/backend-specialist.md +215 -238
- package/.agent/agents/code-archaeologist.md +161 -181
- package/.agent/agents/database-architect.md +184 -207
- package/.agent/agents/debugger.md +191 -218
- package/.agent/agents/dependency-reviewer.md +103 -136
- package/.agent/agents/devops-engineer.md +218 -238
- package/.agent/agents/documentation-writer.md +201 -221
- package/.agent/agents/explorer-agent.md +160 -180
- package/.agent/agents/frontend-reviewer.md +160 -194
- package/.agent/agents/frontend-specialist.md +248 -237
- package/.agent/agents/game-developer.md +48 -52
- package/.agent/agents/logic-reviewer.md +116 -149
- package/.agent/agents/mobile-developer.md +200 -223
- package/.agent/agents/mobile-reviewer.md +162 -195
- package/.agent/agents/orchestrator.md +181 -211
- package/.agent/agents/penetration-tester.md +157 -174
- package/.agent/agents/performance-optimizer.md +183 -203
- package/.agent/agents/performance-reviewer.md +178 -211
- package/.agent/agents/product-manager.md +142 -162
- package/.agent/agents/product-owner.md +6 -25
- package/.agent/agents/project-planner.md +142 -162
- package/.agent/agents/qa-automation-engineer.md +225 -242
- package/.agent/agents/security-auditor.md +174 -194
- package/.agent/agents/seo-specialist.md +193 -213
- package/.agent/agents/sql-reviewer.md +161 -194
- package/.agent/agents/supervisor-agent.md +184 -203
- package/.agent/agents/swarm-worker-contracts.md +17 -17
- package/.agent/agents/swarm-worker-registry.md +46 -46
- package/.agent/agents/test-coverage-reviewer.md +160 -193
- package/.agent/agents/test-engineer.md +0 -21
- package/.agent/agents/type-safety-reviewer.md +175 -208
- package/.agent/patterns/generator.md +9 -9
- package/.agent/patterns/inversion.md +12 -12
- package/.agent/patterns/pipeline.md +9 -9
- package/.agent/patterns/reviewer.md +13 -13
- package/.agent/patterns/tool-wrapper.md +9 -9
- package/.agent/rules/GEMINI.md +63 -63
- package/.agent/scripts/compress_skills.py +167 -0
- package/.agent/scripts/consolidate_skills.py +173 -0
- package/.agent/scripts/deep_compress.py +202 -0
- package/.agent/scripts/minify_context.py +80 -0
- package/.agent/scripts/security_scan.py +1 -1
- package/.agent/scripts/strip_tribunal.py +41 -0
- package/.agent/skills/agent-organizer/SKILL.md +92 -126
- package/.agent/skills/agentic-patterns/SKILL.md +0 -70
- package/.agent/skills/ai-prompt-injection-defense/SKILL.md +126 -160
- package/.agent/skills/api-patterns/SKILL.md +123 -215
- package/.agent/skills/api-security-auditor/SKILL.md +143 -177
- package/.agent/skills/app-builder/SKILL.md +326 -50
- package/.agent/skills/app-builder/templates/SKILL.md +13 -15
- package/.agent/skills/app-builder/templates/astro-static/TEMPLATE.md +16 -16
- package/.agent/skills/app-builder/templates/chrome-extension/TEMPLATE.md +22 -22
- package/.agent/skills/app-builder/templates/cli-tool/TEMPLATE.md +18 -18
- package/.agent/skills/app-builder/templates/electron-desktop/TEMPLATE.md +20 -20
- package/.agent/skills/app-builder/templates/express-api/TEMPLATE.md +17 -17
- package/.agent/skills/app-builder/templates/flutter-app/TEMPLATE.md +18 -18
- package/.agent/skills/app-builder/templates/monorepo-turborepo/TEMPLATE.md +21 -21
- package/.agent/skills/app-builder/templates/nextjs-fullstack/TEMPLATE.md +19 -19
- package/.agent/skills/app-builder/templates/nextjs-saas/TEMPLATE.md +26 -26
- package/.agent/skills/app-builder/templates/nextjs-static/TEMPLATE.md +26 -26
- package/.agent/skills/app-builder/templates/nuxt-app/TEMPLATE.md +19 -19
- package/.agent/skills/app-builder/templates/python-fastapi/TEMPLATE.md +18 -18
- package/.agent/skills/app-builder/templates/react-native-app/TEMPLATE.md +20 -20
- package/.agent/skills/appflow-wireframe/SKILL.md +87 -121
- package/.agent/skills/architecture/SKILL.md +82 -252
- package/.agent/skills/authentication-best-practices/SKILL.md +139 -173
- package/.agent/skills/bash-linux/SKILL.md +120 -154
- package/.agent/skills/behavioral-modes/SKILL.md +8 -69
- package/.agent/skills/brainstorming/SKILL.md +428 -104
- package/.agent/skills/building-native-ui/SKILL.md +143 -174
- package/.agent/skills/clean-code/SKILL.md +323 -360
- package/.agent/skills/code-review-checklist/SKILL.md +0 -62
- package/.agent/skills/config-validator/SKILL.md +107 -141
- package/.agent/skills/csharp-developer/SKILL.md +468 -528
- package/.agent/skills/database-design/SKILL.md +104 -369
- package/.agent/skills/deployment-procedures/SKILL.md +111 -145
- package/.agent/skills/devops-engineer/SKILL.md +295 -332
- package/.agent/skills/devops-incident-responder/SKILL.md +79 -113
- package/.agent/skills/doc.md +5 -5
- package/.agent/skills/documentation-templates/SKILL.md +19 -63
- package/.agent/skills/edge-computing/SKILL.md +123 -157
- package/.agent/skills/extract-design-system/SKILL.md +100 -134
- package/.agent/skills/framer-motion-expert/SKILL.md +111 -855
- package/.agent/skills/frontend-design/SKILL.md +151 -499
- package/.agent/skills/game-design-expert/SKILL.md +71 -105
- package/.agent/skills/game-engineering-expert/SKILL.md +88 -122
- package/.agent/skills/geo-fundamentals/SKILL.md +89 -124
- package/.agent/skills/github-operations/SKILL.md +279 -314
- package/.agent/skills/gsap-expert/SKILL.md +119 -826
- package/.agent/skills/i18n-localization/SKILL.md +104 -138
- package/.agent/skills/intelligent-routing/SKILL.md +159 -127
- package/.agent/skills/lint-and-validate/SKILL.md +8 -52
- package/.agent/skills/llm-engineering/SKILL.md +344 -357
- package/.agent/skills/local-first/SKILL.md +120 -154
- package/.agent/skills/mcp-builder/SKILL.md +84 -118
- package/.agent/skills/mobile-design/SKILL.md +213 -219
- package/.agent/skills/motion-engineering/SKILL.md +184 -0
- package/.agent/skills/nextjs-react-expert/SKILL.md +99 -698
- package/.agent/skills/nodejs-best-practices/SKILL.md +498 -559
- package/.agent/skills/observability/SKILL.md +293 -330
- package/.agent/skills/parallel-agents/SKILL.md +88 -122
- package/.agent/skills/performance-profiling/SKILL.md +217 -254
- package/.agent/skills/plan-writing/SKILL.md +84 -118
- package/.agent/skills/platform-engineer/SKILL.md +89 -123
- package/.agent/skills/playwright-best-practices/SKILL.md +128 -162
- package/.agent/skills/powershell-windows/SKILL.md +112 -146
- package/.agent/skills/python-patterns/SKILL.md +7 -35
- package/.agent/skills/python-pro/SKILL.md +148 -754
- package/.agent/skills/react-specialist/SKILL.md +123 -827
- package/.agent/skills/readme-builder/SKILL.md +15 -85
- package/.agent/skills/realtime-patterns/SKILL.md +269 -304
- package/.agent/skills/red-team-tactics/SKILL.md +10 -51
- package/.agent/skills/rust-pro/SKILL.md +623 -701
- package/.agent/skills/seo-fundamentals/SKILL.md +120 -154
- package/.agent/skills/server-management/SKILL.md +156 -190
- package/.agent/skills/shadcn-ui-expert/SKILL.md +172 -206
- package/.agent/skills/skill-creator/SKILL.md +18 -58
- package/.agent/skills/sql-pro/SKILL.md +579 -633
- package/.agent/skills/supabase-postgres-best-practices/SKILL.md +28 -68
- package/.agent/skills/swiftui-expert/SKILL.md +142 -176
- package/.agent/skills/systematic-debugging/SKILL.md +84 -118
- package/.agent/skills/tailwind-patterns/SKILL.md +516 -576
- package/.agent/skills/tdd-workflow/SKILL.md +103 -137
- package/.agent/skills/test-result-analyzer/SKILL.md +33 -73
- package/.agent/skills/testing-patterns/SKILL.md +512 -573
- package/.agent/skills/trend-researcher/SKILL.md +30 -71
- package/.agent/skills/ui-ux-pro-max/SKILL.md +0 -41
- package/.agent/skills/ui-ux-researcher/SKILL.md +51 -91
- package/.agent/skills/vue-expert/SKILL.md +127 -866
- package/.agent/skills/vulnerability-scanner/SKILL.md +354 -269
- package/.agent/skills/web-accessibility-auditor/SKILL.md +159 -193
- package/.agent/skills/web-design-guidelines/SKILL.md +17 -61
- package/.agent/skills/webapp-testing/SKILL.md +111 -145
- package/.agent/skills/whimsy-injector/SKILL.md +58 -132
- package/.agent/skills/workflow-optimizer/SKILL.md +28 -68
- package/.agent/workflows/api-tester.md +151 -151
- package/.agent/workflows/audit.md +127 -138
- package/.agent/workflows/brainstorm.md +110 -110
- package/.agent/workflows/changelog.md +112 -112
- package/.agent/workflows/create.md +124 -124
- package/.agent/workflows/debug.md +165 -189
- package/.agent/workflows/deploy.md +180 -189
- package/.agent/workflows/enhance.md +128 -151
- package/.agent/workflows/fix.md +114 -135
- package/.agent/workflows/generate.md +12 -4
- package/.agent/workflows/migrate.md +160 -160
- package/.agent/workflows/orchestrate.md +168 -168
- package/.agent/workflows/performance-benchmarker.md +114 -123
- package/.agent/workflows/plan.md +173 -173
- package/.agent/workflows/preview.md +80 -80
- package/.agent/workflows/refactor.md +161 -183
- package/.agent/workflows/review-ai.md +101 -129
- package/.agent/workflows/review.md +116 -116
- package/.agent/workflows/session.md +94 -94
- package/.agent/workflows/status.md +79 -79
- package/.agent/workflows/strengthen-skills.md +138 -139
- package/.agent/workflows/swarm.md +179 -179
- package/.agent/workflows/test.md +189 -211
- package/.agent/workflows/tribunal-backend.md +93 -113
- package/.agent/workflows/tribunal-database.md +94 -115
- package/.agent/workflows/tribunal-frontend.md +95 -118
- package/.agent/workflows/tribunal-full.md +92 -133
- package/.agent/workflows/tribunal-mobile.md +94 -119
- package/.agent/workflows/tribunal-performance.md +109 -133
- package/.agent/workflows/ui-ux-pro-max.md +122 -143
- package/package.json +1 -1
- package/.agent/skills/api-patterns/api-style.md +0 -42
- package/.agent/skills/api-patterns/auth.md +0 -24
- package/.agent/skills/api-patterns/documentation.md +0 -26
- package/.agent/skills/api-patterns/graphql.md +0 -41
- package/.agent/skills/api-patterns/rate-limiting.md +0 -31
- package/.agent/skills/api-patterns/response.md +0 -37
- package/.agent/skills/api-patterns/rest.md +0 -40
- package/.agent/skills/api-patterns/security-testing.md +0 -122
- package/.agent/skills/api-patterns/trpc.md +0 -41
- package/.agent/skills/api-patterns/versioning.md +0 -22
- package/.agent/skills/app-builder/agent-coordination.md +0 -71
- package/.agent/skills/app-builder/feature-building.md +0 -53
- package/.agent/skills/app-builder/project-detection.md +0 -34
- package/.agent/skills/app-builder/scaffolding.md +0 -118
- package/.agent/skills/app-builder/tech-stack.md +0 -40
- package/.agent/skills/architecture/context-discovery.md +0 -43
- package/.agent/skills/architecture/examples.md +0 -94
- package/.agent/skills/architecture/pattern-selection.md +0 -68
- package/.agent/skills/architecture/patterns-reference.md +0 -50
- package/.agent/skills/architecture/trade-off-analysis.md +0 -77
- package/.agent/skills/brainstorming/dynamic-questioning.md +0 -360
- package/.agent/skills/database-design/database-selection.md +0 -43
- package/.agent/skills/database-design/indexing.md +0 -39
- package/.agent/skills/database-design/migrations.md +0 -48
- package/.agent/skills/database-design/optimization.md +0 -36
- package/.agent/skills/database-design/orm-selection.md +0 -30
- package/.agent/skills/database-design/schema-design.md +0 -56
- package/.agent/skills/frontend-design/animation-guide.md +0 -331
- package/.agent/skills/frontend-design/color-system.md +0 -329
- package/.agent/skills/frontend-design/decision-trees.md +0 -418
- package/.agent/skills/frontend-design/motion-graphics.md +0 -306
- package/.agent/skills/frontend-design/typography-system.md +0 -363
- package/.agent/skills/frontend-design/ux-psychology.md +0 -1116
- package/.agent/skills/frontend-design/visual-effects.md +0 -383
- package/.agent/skills/intelligent-routing/router-manifest.md +0 -65
- package/.agent/skills/mobile-design/decision-trees.md +0 -516
- package/.agent/skills/mobile-design/mobile-backend.md +0 -491
- package/.agent/skills/mobile-design/mobile-color-system.md +0 -420
- package/.agent/skills/mobile-design/mobile-debugging.md +0 -122
- package/.agent/skills/mobile-design/mobile-design-thinking.md +0 -357
- package/.agent/skills/mobile-design/mobile-navigation.md +0 -458
- package/.agent/skills/mobile-design/mobile-performance.md +0 -767
- package/.agent/skills/mobile-design/mobile-testing.md +0 -356
- package/.agent/skills/mobile-design/mobile-typography.md +0 -433
- package/.agent/skills/mobile-design/platform-android.md +0 -666
- package/.agent/skills/mobile-design/platform-ios.md +0 -561
- package/.agent/skills/mobile-design/touch-psychology.md +0 -537
- package/.agent/skills/nextjs-react-expert/1-async-eliminating-waterfalls.md +0 -312
- package/.agent/skills/nextjs-react-expert/2-bundle-bundle-size-optimization.md +0 -240
- package/.agent/skills/nextjs-react-expert/3-server-server-side-performance.md +0 -490
- package/.agent/skills/nextjs-react-expert/4-client-client-side-data-fetching.md +0 -264
- package/.agent/skills/nextjs-react-expert/5-rerender-re-render-optimization.md +0 -581
- package/.agent/skills/nextjs-react-expert/6-rendering-rendering-performance.md +0 -432
- package/.agent/skills/nextjs-react-expert/7-js-javascript-performance.md +0 -684
- package/.agent/skills/nextjs-react-expert/8-advanced-advanced-patterns.md +0 -150
- package/.agent/skills/vulnerability-scanner/checklists.md +0 -121
|
@@ -1,208 +1,74 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: vue-expert
|
|
3
|
-
description: Vue 3.5+ Composition API
|
|
3
|
+
description: Vue 3.5+ Composition API. Script setup, reactive refs, computed, watchers, composables, Pinia, Vue Router 4, Nuxt 4. Use when building Vue/Nuxt applications.
|
|
4
4
|
allowed-tools: Read, Write, Edit, Glob, Grep
|
|
5
|
-
version:
|
|
6
|
-
last-updated: 2026-
|
|
7
|
-
applies-to-model: gemini-2.5-pro, claude-3-7-sonnet
|
|
5
|
+
version: 3.1.0
|
|
6
|
+
last-updated: 2026-04-06
|
|
8
7
|
---
|
|
9
8
|
|
|
10
|
-
# Vue
|
|
9
|
+
# Vue 3.5+ & Nuxt 4 — Dense Reference
|
|
11
10
|
|
|
12
|
-
|
|
13
|
-
|
|
11
|
+
## Hallucination Traps (Read First)
|
|
12
|
+
- ❌ Options API (`data()`, `methods:`, `computed:`) → ✅ `<script setup lang="ts">`
|
|
13
|
+
- ❌ `defineComponent()` with `<script setup>` → ✅ redundant, skip it
|
|
14
|
+
- ❌ `defineModel` in Vue < 3.4 → ✅ added in 3.4+
|
|
15
|
+
- ❌ `ref.value` in template → ✅ auto-unwrapped in template (no `.value`)
|
|
16
|
+
- ❌ `reactive()` for primitives → ✅ use `ref()` — `reactive()` breaks on reassign
|
|
17
|
+
- ❌ `watch(state.count, ...)` (primitive) → ✅ `watch(() => state.count, ...)`
|
|
18
|
+
- ❌ `onBeforeMount` for data fetch → ✅ use `await` directly in `<script setup>` + `<Suspense>`
|
|
19
|
+
- ❌ Pinia `this.$store` → ✅ `useStore()` from `pinia`
|
|
20
|
+
- ❌ `useRoute()` / `useRouter()` outside setup → ✅ only works inside `<script setup>` or composables
|
|
14
21
|
|
|
15
22
|
---
|
|
16
23
|
|
|
17
|
-
##
|
|
24
|
+
## `<script setup>` — The Only Way
|
|
18
25
|
|
|
19
26
|
```vue
|
|
20
27
|
<script setup lang="ts">
|
|
21
|
-
|
|
22
|
-
// It compiles to a render function with zero boilerplate
|
|
23
|
-
|
|
24
|
-
import { ref, computed, onMounted } from "vue";
|
|
25
|
-
import { useRouter } from "vue-router";
|
|
28
|
+
import { ref, computed, watch, onMounted } from "vue";
|
|
26
29
|
|
|
27
30
|
// Props
|
|
28
|
-
const props = defineProps<{
|
|
29
|
-
|
|
30
|
-
|
|
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
|
-
});
|
|
31
|
+
const props = defineProps<{ title: string; count?: number }>();
|
|
32
|
+
// With defaults:
|
|
33
|
+
const props = withDefaults(defineProps<{ variant?: "primary" | "secondary" }>(), { variant: "primary" });
|
|
56
34
|
|
|
57
|
-
//
|
|
58
|
-
const
|
|
59
|
-
const count = defineModel<number>("count"); // named v-model
|
|
35
|
+
// Emits
|
|
36
|
+
const emit = defineEmits<{ update: [value: string]; delete: [id: number] }>();
|
|
60
37
|
|
|
61
|
-
//
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
// <script setup> IS the setup function — defineComponent is redundant
|
|
65
|
-
</script>
|
|
66
|
-
```
|
|
38
|
+
// v-model (Vue 3.4+)
|
|
39
|
+
const modelValue = defineModel<string>(); // default model
|
|
40
|
+
const count = defineModel<number>("count"); // named model
|
|
67
41
|
|
|
68
|
-
|
|
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"));
|
|
42
|
+
// Expose to parent ref
|
|
43
|
+
defineExpose({ reset: () => {}, focus: () => {} });
|
|
89
44
|
</script>
|
|
90
|
-
|
|
91
|
-
<!-- ❌ HALLUCINATION TRAP: Never generate Options API in Vue 3.5+ projects
|
|
92
|
-
unless explicitly maintaining legacy code -->
|
|
93
45
|
```
|
|
94
46
|
|
|
95
47
|
---
|
|
96
48
|
|
|
97
|
-
## Reactivity
|
|
98
|
-
|
|
99
|
-
### ref vs reactive
|
|
49
|
+
## Reactivity
|
|
100
50
|
|
|
101
51
|
```ts
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
// ref — single value (primitive or object)
|
|
52
|
+
// ref — for primitives and objects (access via .value in JS, auto-unwrap in template)
|
|
105
53
|
const count = ref(0);
|
|
106
|
-
count.value++;
|
|
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
|
|
54
|
+
count.value++;
|
|
118
55
|
|
|
119
|
-
//
|
|
120
|
-
const
|
|
56
|
+
// reactive — for objects (loses reactivity on reassign/destructure)
|
|
57
|
+
const state = reactive({ name: "Alice", age: 25 });
|
|
58
|
+
// ❌ const { name } = state; // loses reactivity
|
|
59
|
+
// ✅ const name = computed(() => state.name);
|
|
121
60
|
|
|
122
|
-
//
|
|
123
|
-
const
|
|
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
|
|
61
|
+
// computed — cached, re-runs only when deps change
|
|
62
|
+
const doubled = computed(() => count.value * 2);
|
|
145
63
|
const fullName = computed({
|
|
146
|
-
get: () => `${
|
|
147
|
-
set: (
|
|
148
|
-
const [first, ...rest] = val.split(" ");
|
|
149
|
-
firstName.value = first;
|
|
150
|
-
lastName.value = rest.join(" ");
|
|
151
|
-
},
|
|
64
|
+
get: () => `${first.value} ${last.value}`,
|
|
65
|
+
set: (v) => { [first.value, last.value] = v.split(" "); },
|
|
152
66
|
});
|
|
153
|
-
```
|
|
154
|
-
|
|
155
|
-
### Watchers
|
|
156
|
-
|
|
157
|
-
```ts
|
|
158
|
-
import { ref, watch, watchEffect, watchPostEffect } from "vue";
|
|
159
67
|
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
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
|
-
});
|
|
68
|
+
// watch
|
|
69
|
+
watch(count, (newVal, oldVal) => {}); // immediate: false by default
|
|
70
|
+
watch(() => props.id, fetchUser, { immediate: true });
|
|
71
|
+
watchEffect(() => { console.log(count.value); }); // auto-tracks deps
|
|
206
72
|
```
|
|
207
73
|
|
|
208
74
|
---
|
|
@@ -210,431 +76,117 @@ watchEffect((onCleanup) => {
|
|
|
210
76
|
## Composables (Custom Hooks)
|
|
211
77
|
|
|
212
78
|
```ts
|
|
213
|
-
//
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
isLoading: Ref<boolean>;
|
|
220
|
-
refresh: () => Promise<void>;
|
|
79
|
+
// useCounter.ts
|
|
80
|
+
export function useCounter(initial = 0) {
|
|
81
|
+
const count = ref(initial);
|
|
82
|
+
const increment = () => count.value++;
|
|
83
|
+
const reset = () => (count.value = initial);
|
|
84
|
+
return { count: readonly(count), increment, reset };
|
|
221
85
|
}
|
|
222
86
|
|
|
223
|
-
|
|
224
|
-
|
|
87
|
+
// useAsyncData.ts
|
|
88
|
+
export function useAsyncData<T>(fn: () => Promise<T>) {
|
|
89
|
+
const data = ref<T | null>(null);
|
|
225
90
|
const error = ref<Error | null>(null);
|
|
226
|
-
const
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
error.value =
|
|
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
|
-
}
|
|
91
|
+
const loading = ref(false);
|
|
92
|
+
async function execute() {
|
|
93
|
+
loading.value = true;
|
|
94
|
+
try { data.value = await fn(); }
|
|
95
|
+
catch (e) { error.value = e as Error; }
|
|
96
|
+
finally { loading.value = false; }
|
|
242
97
|
}
|
|
243
|
-
|
|
244
|
-
|
|
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;
|
|
98
|
+
execute();
|
|
99
|
+
return { data, error, loading, refresh: execute };
|
|
299
100
|
}
|
|
300
|
-
|
|
301
|
-
// Usage:
|
|
302
|
-
const query = ref("");
|
|
303
|
-
const debouncedQuery = useDebounce(query, 500);
|
|
304
|
-
// debouncedQuery only updates 500ms after the user stops typing
|
|
305
101
|
```
|
|
306
102
|
|
|
307
103
|
---
|
|
308
104
|
|
|
309
|
-
## Pinia
|
|
310
|
-
|
|
311
|
-
### Setup Store (Recommended)
|
|
105
|
+
## Pinia
|
|
312
106
|
|
|
313
107
|
```ts
|
|
314
|
-
// stores/
|
|
108
|
+
// stores/counter.ts
|
|
315
109
|
import { defineStore } from "pinia";
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
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 };
|
|
110
|
+
export const useCounterStore = defineStore("counter", () => {
|
|
111
|
+
const count = ref(0); // Setup Store (preferred)
|
|
112
|
+
const doubled = computed(() => count.value * 2);
|
|
113
|
+
function increment() { count.value++; }
|
|
114
|
+
return { count, doubled, increment };
|
|
363
115
|
});
|
|
364
116
|
|
|
365
|
-
//
|
|
366
|
-
|
|
367
|
-
// ❌
|
|
368
|
-
// ✅
|
|
369
|
-
```
|
|
370
|
-
|
|
371
|
-
### Using Stores in Components
|
|
372
|
-
|
|
373
|
-
```vue
|
|
374
|
-
<script setup lang="ts">
|
|
375
|
-
import { useCartStore } from "@/stores/cart";
|
|
117
|
+
// Usage in component:
|
|
118
|
+
const store = useCounterStore();
|
|
119
|
+
// ❌ const { count } = store; // loses reactivity!
|
|
120
|
+
// ✅ const count = storeToRefs(store).count;
|
|
376
121
|
import { storeToRefs } from "pinia";
|
|
122
|
+
const { count } = storeToRefs(store);
|
|
377
123
|
|
|
378
|
-
|
|
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
|
|
124
|
+
// Persist plugin:
|
|
407
125
|
import { createPinia } from "pinia";
|
|
408
126
|
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
|
-
});
|
|
127
|
+
const pinia = createPinia().use(piniaPluginPersistedstate);
|
|
421
128
|
```
|
|
422
129
|
|
|
423
130
|
---
|
|
424
131
|
|
|
425
132
|
## Vue Router 4
|
|
426
133
|
|
|
427
|
-
### Route Configuration
|
|
428
|
-
|
|
429
134
|
```ts
|
|
430
135
|
// router/index.ts
|
|
431
|
-
import { createRouter, createWebHistory
|
|
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
|
-
|
|
136
|
+
import { createRouter, createWebHistory } from "vue-router";
|
|
459
137
|
const router = createRouter({
|
|
460
138
|
history: createWebHistory(),
|
|
461
|
-
routes
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
139
|
+
routes: [
|
|
140
|
+
{ path: "/", component: () => import("./views/Home.vue") }, // lazy-loaded
|
|
141
|
+
{ path: "/user/:id", component: UserView, props: true }, // props:true passes params as props
|
|
142
|
+
{ path: "/:pathMatch(.*)*", component: NotFound }, // 404 catch-all
|
|
143
|
+
],
|
|
465
144
|
});
|
|
466
|
-
|
|
467
|
-
// Navigation guard
|
|
145
|
+
// Route guards
|
|
468
146
|
router.beforeEach(async (to, from) => {
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
if (to.meta.requiresAuth && !authStore.isAuthenticated) {
|
|
472
|
-
return { name: "login", query: { redirect: to.fullPath } };
|
|
473
|
-
}
|
|
147
|
+
if (to.meta.requiresAuth && !isLoggedIn()) return { name: "Login" };
|
|
474
148
|
});
|
|
475
149
|
|
|
476
|
-
|
|
477
|
-
```
|
|
478
|
-
|
|
479
|
-
### Router Composables
|
|
480
|
-
|
|
481
|
-
```vue
|
|
482
|
-
<script setup lang="ts">
|
|
150
|
+
// In component:
|
|
483
151
|
import { useRouter, useRoute } from "vue-router";
|
|
484
|
-
|
|
485
152
|
const router = useRouter();
|
|
486
153
|
const route = useRoute();
|
|
487
|
-
|
|
488
|
-
|
|
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>
|
|
154
|
+
router.push({ name: "User", params: { id: 42 } });
|
|
155
|
+
const userId = route.params.id as string;
|
|
504
156
|
```
|
|
505
157
|
|
|
506
158
|
---
|
|
507
159
|
|
|
508
|
-
##
|
|
509
|
-
|
|
510
|
-
### Slots
|
|
160
|
+
## Templates
|
|
511
161
|
|
|
512
162
|
```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
163
|
<template>
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
164
|
+
<!-- v-model -->
|
|
165
|
+
<input v-model="email" />
|
|
166
|
+
<MyInput v-model:title="title" v-model:count="count" /> <!-- named model -->
|
|
167
|
+
|
|
168
|
+
<!-- v-for with key (ALWAYS set key) -->
|
|
169
|
+
<li v-for="item in items" :key="item.id">{{ item.name }}</li>
|
|
170
|
+
|
|
171
|
+
<!-- Dynamic components -->
|
|
172
|
+
<component :is="currentTab" />
|
|
173
|
+
|
|
174
|
+
<!-- Teleport — render in a different DOM node -->
|
|
175
|
+
<Teleport to="body"><Modal v-if="showModal" /></Teleport>
|
|
176
|
+
|
|
177
|
+
<!-- Transition -->
|
|
178
|
+
<Transition name="fade" mode="out-in">
|
|
179
|
+
<component :is="view" :key="view" />
|
|
180
|
+
</Transition>
|
|
181
|
+
|
|
182
|
+
<!-- Suspense (async components / composables with await) -->
|
|
183
|
+
<Suspense><AsyncComponent /><template #fallback>Loading...</template></Suspense>
|
|
541
184
|
</template>
|
|
542
185
|
|
|
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
186
|
<style>
|
|
612
|
-
|
|
613
|
-
|
|
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
|
-
}
|
|
187
|
+
/* Transition CSS */
|
|
188
|
+
.fade-enter-active, .fade-leave-active { transition: opacity 0.3s; }
|
|
189
|
+
.fade-enter-from, .fade-leave-to { opacity: 0; }
|
|
638
190
|
</style>
|
|
639
191
|
```
|
|
640
192
|
|
|
@@ -642,323 +194,32 @@ if (!themeCtx) throw new Error("ThemeKey not provided");
|
|
|
642
194
|
|
|
643
195
|
## Nuxt 4
|
|
644
196
|
|
|
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
197
|
```
|
|
788
|
-
|
|
789
|
-
|
|
790
|
-
|
|
791
|
-
|
|
792
|
-
|
|
793
|
-
|
|
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>
|
|
198
|
+
auto-imports: ref, computed, useRoute, useFetch — no imports needed
|
|
199
|
+
composables/: auto-imported by filename
|
|
200
|
+
server/api/: server routes (GET/POST)
|
|
201
|
+
pages/: file-based routing
|
|
202
|
+
layouts/: layout components
|
|
203
|
+
middleware/: route guards
|
|
821
204
|
```
|
|
822
205
|
|
|
823
|
-
---
|
|
824
|
-
|
|
825
|
-
## Testing with Vitest
|
|
826
|
-
|
|
827
206
|
```ts
|
|
828
|
-
//
|
|
829
|
-
|
|
830
|
-
|
|
831
|
-
|
|
832
|
-
|
|
833
|
-
|
|
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
|
-
});
|
|
207
|
+
// pages/users/[id].vue
|
|
208
|
+
const { id } = useRoute().params; // auto-imported
|
|
209
|
+
const { data, error, refresh } = await useFetch(`/api/users/${id}`, {
|
|
210
|
+
lazy: false, // SSR: wait for data before rendering
|
|
211
|
+
server: true, // fetch on server (default)
|
|
212
|
+
transform: (r) => r.user,
|
|
851
213
|
});
|
|
852
|
-
|
|
853
|
-
//
|
|
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]
|
|
214
|
+
// ❌ TRAP: useFetch in Nuxt ≠ @tanstack/react-query. It's Nuxt-specific.
|
|
215
|
+
// ❌ TRAP: useAsyncData key must be UNIQUE per page/component
|
|
908
216
|
```
|
|
909
217
|
|
|
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
218
|
---
|
|
931
219
|
|
|
932
|
-
##
|
|
933
|
-
|
|
934
|
-
|
|
935
|
-
|
|
936
|
-
|
|
937
|
-
|
|
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.
|
|
220
|
+
## Performance
|
|
221
|
+
- ✅ Use `v-memo` for expensive list items that rarely change
|
|
222
|
+
- ✅ `defineAsyncComponent(() => import("./Heavy.vue"))` for code splitting
|
|
223
|
+
- ✅ `:key` on `<component :is>` forces re-mount on route change (prevents stale state)
|
|
224
|
+
- ❌ Avoid deeply nested reactive objects — use `shallowRef`/`shallowReactive` for large data
|
|
225
|
+
- ❌ Never mutate props — emit events instead
|