` for conditional groups, not wrapper ``.
+- `v-if` / `v-else-if` / `v-else` must be on consecutive sibling elements.
+- Never put multi-line logic inline in templates — extract to computed or method.
+
+```vue
+
+
{{ greeting }}
+
+
+{{ user.isAdmin ? "Welcome, admin" : `Hello ${user.name}` }}
+```
+
+## Imports
+
+- Vue imports first: `import { ref, computed } from "vue"`
+- Then ecosystem packages (vue-router, pinia), then absolute project imports, then relative
+- Type-only imports: `import type { User } from "@/types"`
+- Auto-imported functions (Nuxt, unplugin-auto-import) must still be explicitly imported when the project does not use auto-import.
+
+## Script vs Template
+
+- Keep `
+```
+
+**Important limitation (all Vue 3.5+ versions)**: You cannot `watch()` a destructured prop variable directly — must wrap in a getter:
+
+```ts
+// WRONG: direct watch on destructured prop (compile-time error in Vue 3.5+)
+watch(count, (newVal) => { ... });
+
+// CORRECT: getter wrapper
+watch(() => count, (newVal) => { ... });
+```
+
+When passing a destructured prop to a composable that needs reactivity, wrap in a getter and use `toValue()` inside the composable:
+
+```ts
+useDynamicCount(() => count); // ✅ preserves reactivity
+```
+
+### Replacing reactive() Objects
+
+```ts
+// WRONG: breaks reactivity
+let state = reactive({ a: 1, b: 2 });
+state = reactive({ a: 3, b: 4 }); // new object, old watchers lost
+
+// CORRECT: mutate in place
+Object.assign(state, { a: 3, b: 4 });
+
+// BETTER: use ref for values that get replaced
+const state = ref({ a: 1, b: 2 });
+state.value = { a: 3, b: 4 }; // reactivity preserved
+```
+
+### `.value` in Script vs Template
+
+```vue
+
+
+
+
+ {{ count }}
+ Increment
+
+```
+
+## `computed()` Rules
+
+- Computed getters must be pure — no side effects (no state mutation, API calls, DOM writes).
+- Never mutate other state inside a computed getter.
+- Computed setter must be paired with a getter — don't create write-only computeds.
+
+```ts
+// CORRECT: pure getter
+const fullName = computed(() => `${firstName.value} ${lastName.value}`);
+
+// CORRECT: with setter
+const fullName = computed({
+ get: () => `${firstName.value} ${lastName.value}`,
+ set: (val: string) => {
+ const [first, last] = val.split(" ");
+ firstName.value = first;
+ lastName.value = last;
+ },
+});
+
+// WRONG: side effect in computed
+const displayName = computed(() => {
+ analytics.track("name-computed"); // ❌ side effect
+ return user.value.name;
+});
+```
+
+## `watch()` vs `watchEffect()`
+
+| Feature | `watch()` | `watchEffect()` |
+|---------|-----------|-----------------|
+| Explicit source | Yes — declare what to track | No — auto-tracks dependencies |
+| Access to old/new values | Yes | No |
+| Initial run | Optional (`immediate: true`) | Always runs immediately |
+| Use case | Side effect on specific data change | Sync reactive state to external system |
+
+```ts
+// watch: explicit, has old/new
+watch(
+ () => props.userId,
+ (newId, oldId) => {
+ fetchUser(newId);
+ }
+);
+
+// watchEffect: auto-tracking, immediate
+watchEffect(() => {
+ console.log(`User ${userId.value} is ${status.value}`);
+});
+```
+
+## Watcher Source Pitfalls
+
+```ts
+// WRONG: watching a ref object (never changes)
+const u = ref({ name: "Alice" });
+watch(u, (val) => {}); // ❌ watches the ref wrapper, not the value
+
+// CORRECT: getter returning .value
+watch(() => u.value, (val) => {});
+
+// ALSO WRONG: reactive getter that doesn't track
+watch(() => state.name, (val) => {}); // ❌ val is snapshot at setup
+
+// CORRECT: getter that accesses property on reactive object
+watch(() => state.name, (val) => {}); // ✅ .name access inside getter is tracked
+// Wait — careful: `() => state.name` DOES track correctly because the getter
+// accesses `.name` on the reactive proxy. The getter is re-evaluated by Vue.
+
+// ACTUALLY WRONG case: direct reactive property
+watch(state.name, ...); // ❌ state.name evaluates to a primitive, not trackable
+
+// CORRECT: getter returning reactive property
+watch(() => state.name, (newName) => { ... });
+```
+
+## Cleanup
+
+Every watcher that creates subscriptions, intervals, or fetch requests must clean up.
+
+**Vue 3.5+**: Use `onWatcherCleanup()` (globally importable from `vue`) for watcher-side-effect cleanup:
+
+```ts
+import { watch, onWatcherCleanup } from "vue";
+
+watch(userId, async (newId) => {
+ const controller = new AbortController();
+ onWatcherCleanup(() => controller.abort());
+ const data = await fetch(`/api/users/${newId}`, { signal: controller.signal });
+ user.value = await data.json();
+});
+```
+
+**All Vue 3 versions**: The watcher callback also receives an `onCleanup` parameter:
+
+```ts
+// watch callback receives an onCleanup function
+watch(userId, async (newId, _oldId, onCleanup) => {
+ const controller = new AbortController();
+ onCleanup(() => controller.abort());
+ const data = await fetch(`/api/users/${newId}`, { signal: controller.signal });
+ user.value = await data.json();
+});
+
+// watchEffect also receives onCleanup
+watchEffect((onCleanup) => {
+ const id = setInterval(tick, 1000);
+ onCleanup(() => clearInterval(id));
+});
+```
+
+## `useTemplateRef()` (Vue 3.5+)
+
+Use `useTemplateRef()` instead of matching a plain `ref` variable name to the template `ref` attribute. It supports dynamic ref IDs and provides better type safety.
+
+```vue
+
+
+
+
+
+```
+
+- The string passed to `useTemplateRef()` must match the `ref` attribute value in the template, **not** the variable name.
+- `@vue/language-tools` 2.1+ provides auto-completion and warnings for `useTemplateRef`.
+
+## Composable Conventions
+
+### Must start with `use`
+
+```ts
+// CORRECT
+export function useDebounce(value: Ref, delay: number): Ref { ... }
+
+// WRONG
+export function debounce(value: Ref, delay: number): Ref { ... }
+```
+
+### Return reactive values
+
+Composables must return `ref()` / `computed()` / `reactive()` so the consumer stays reactive. Never return a raw primitive or plain object snapshot.
+
+```ts
+// CORRECT
+export function useCounter() {
+ const count = ref(0);
+ const doubled = computed(() => count.value * 2);
+ function increment() { count.value++; }
+ return { count, doubled, increment };
+}
+
+// WRONG: returns snapshot
+export function useCounter() {
+ let count = 0;
+ function increment() { count++; }
+ return { count, increment }; // count is a plain number — not reactive
+}
+```
+
+### Accept reactive inputs gracefully
+
+When a composable accepts reactive data, use `toRef()` / `toValue()` (Vue 3.3+) so callers can pass either a ref or a plain value.
+
+```ts
+export function useTitle(newTitle: MaybeRef) {
+ const title = toRef(newTitle);
+ watchEffect(() => {
+ document.title = title.value;
+ });
+}
+
+// Caller can pass either:
+useTitle("Home"); // plain value
+useTitle(ref("Home")); // ref
+useTitle(computed(...)); // computed
+```
+
+### Side effects must be scoped
+
+Composables that create side effects (event listeners, timers, subscriptions) must:
+
+1. Only run when the component using them is mounted — use `onMounted` / `watch`.
+2. Clean up automatically — use `onUnmounted` or watcher `onCleanup`.
+
+```ts
+export function useEventListener(
+ event: K,
+ handler: (e: WindowEventMap[K]) => void,
+) {
+ onMounted(() => window.addEventListener(event, handler));
+ onUnmounted(() => window.removeEventListener(event, handler));
+}
+```
+
+### No module-scope side effects
+
+Never initialize state, start timers, or subscribe to external systems in the module scope of a composable file — it runs once regardless of component instance count.
+
+```ts
+// WRONG: module scope side effect
+const globalCount = ref(0); // ❌ shared across all components
+setInterval(() => globalCount.value++, 1000);
+
+export function useGlobalCount() {
+ return globalCount;
+}
+
+// CORRECT: scoped to each invocation
+export function useInterval(fn: () => void, ms: number) {
+ onMounted(() => {
+ const id = setInterval(fn, ms);
+ onUnmounted(() => clearInterval(id));
+ });
+}
+```
+
+## `shallowRef()` and `shallowReactive()`
+
+Use `shallowRef()` for large immutable data structures that are replaced as a whole — avoids the deep reactivity overhead.
+
+```ts
+const items = shallowRef- ([]);
+// items.value = await fetchItems(); // replacement works
+// items.value[0].name = "new"; // ❌ inner mutations are NOT reactive
+```
+
+Use `shallowReactive()` when only top-level properties should be reactive.
+
+## Lint Configuration
+
+Required rules:
+
+```json
+{
+ "rules": {
+ "vue/no-ref-as-operand": "error",
+ "vue/no-mutating-props": "error",
+ "vue/return-in-computed-property": "error"
+ }
+}
+```