@gallopsystems/agent-skills 1.5.0 → 1.6.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (31) hide show
  1. package/README.md +2 -1
  2. package/package.json +1 -1
  3. package/plugins/nuxt-nitro-api/.claude-plugin/plugin.json +1 -1
  4. package/plugins/nuxt-nitro-api/skills/nuxt-nitro-api/SKILL.md +23 -8
  5. package/plugins/nuxt-nitro-api/skills/nuxt-nitro-api/auth-patterns.md +1 -0
  6. package/plugins/nuxt-nitro-api/skills/nuxt-nitro-api/caching.md +68 -0
  7. package/plugins/nuxt-nitro-api/skills/nuxt-nitro-api/composables-utils.md +31 -1
  8. package/plugins/nuxt-nitro-api/skills/nuxt-nitro-api/error-handling.md +74 -0
  9. package/plugins/nuxt-nitro-api/skills/nuxt-nitro-api/fetch-patterns.md +18 -0
  10. package/plugins/nuxt-nitro-api/skills/nuxt-nitro-api/formatters.md +11 -0
  11. package/plugins/nuxt-nitro-api/skills/nuxt-nitro-api/layers.md +40 -0
  12. package/plugins/nuxt-nitro-api/skills/nuxt-nitro-api/page-structure.md +1 -0
  13. package/plugins/nuxt-nitro-api/skills/nuxt-nitro-api/route-rules.md +41 -0
  14. package/plugins/nuxt-nitro-api/skills/nuxt-nitro-api/server-runtime.md +97 -0
  15. package/plugins/nuxt-nitro-api/skills/nuxt-nitro-api/ssr-client.md +60 -0
  16. package/plugins/nuxt-nitro-api/skills/nuxt-nitro-api/state-management.md +68 -0
  17. package/plugins/nuxt-nitro-api/skills/nuxt-nitro-api/storage.md +59 -0
  18. package/plugins/volt-primevue/skills/volt-primevue/SKILL.md +56 -12
  19. package/plugins/volt-primevue/skills/volt-primevue/config.md +51 -0
  20. package/plugins/volt-primevue/skills/volt-primevue/gotchas.md +49 -0
  21. package/plugins/volt-primevue/skills/volt-primevue/theming.md +20 -6
  22. package/plugins/vue-nuxt/.claude-plugin/plugin.json +8 -0
  23. package/plugins/vue-nuxt/skills/vue-nuxt/SKILL.md +48 -0
  24. package/plugins/vue-nuxt/skills/vue-nuxt/auto-imports.md +48 -0
  25. package/plugins/vue-nuxt/skills/vue-nuxt/component-authoring.md +159 -0
  26. package/plugins/vue-nuxt/skills/vue-nuxt/composables.md +95 -0
  27. package/plugins/vue-nuxt/skills/vue-nuxt/reactivity.md +133 -0
  28. package/plugins/vue-nuxt/skills/vue-nuxt/slots.md +139 -0
  29. package/plugins/vue-nuxt/skills/vue-nuxt/template-idioms.md +142 -0
  30. package/plugins/vue-nuxt/skills/vue-nuxt/v-model.md +106 -0
  31. package/plugins/vue-nuxt/skills/vue-nuxt/watch.md +194 -0
@@ -0,0 +1,194 @@
1
+ # `watch` — the escape hatch, not the default
2
+
3
+ `watch` reactively performs **side effects**. It is not for deriving values —
4
+ that's `computed`. Heavy `watch` use is usually a `computed`, a *writable*
5
+ `computed`, a `defineModel`, or an event handler in disguise.
6
+
7
+ **Rule of thumb:** read the watcher body. If it ends by assigning one reactive
8
+ value from others and nothing leaves Vue's world (no fetch, DOM, third-party lib,
9
+ storage, URL), delete it and write a `computed`.
10
+
11
+ > Grounded in a 159-watcher audit across 7 Nuxt apps: **77% were legitimate, ~9%
12
+ > clear smells, ~14% borderline.** The smells clustered into the four shapes
13
+ > below; over half the *borderline* cases were one pattern ("reset a field in a
14
+ > watcher"). The legit cases are real — the trap is using `watch` for what the
15
+ > framework already does declaratively.
16
+
17
+ ## Writing a value back? Use a writable `computed`, not a watch
18
+
19
+ The most common reason people reach for `watch` is to *write a value back* —
20
+ mirror a prop, transform a bound value, proxy a child's `v-model`. A `computed`
21
+ can have a setter; use it.
22
+
23
+ ```ts
24
+ // ❌ two watches mirroring a prop both ways — desyncs, pure boilerplate
25
+ const visible = ref(props.visible)
26
+ watch(() => props.visible, (v) => (visible.value = v))
27
+ watch(visible, (v) => emit('update:visible', v))
28
+
29
+ // ✅ writable computed — one source of truth, genuinely two-way
30
+ const visible = computed({
31
+ get: () => props.visible,
32
+ set: (v) => emit('update:visible', v),
33
+ })
34
+ // ✅ or, for a component's OWN model: const visible = defineModel<boolean>('visible')
35
+ ```
36
+
37
+ A writable `computed` also transforms a value across the boundary (string ↔ Date):
38
+
39
+ ```ts
40
+ const date = computed({
41
+ get: () => parseISO(model.value),
42
+ set: (d) => (model.value = d.toISOString()),
43
+ })
44
+ ```
45
+
46
+ (Both getter and setter receive the previous value as their first argument if you
47
+ need it.)
48
+
49
+ Two rules from the Vue docs that keep this correct:
50
+
51
+ - **Getters must be side-effect-free.** A `computed` getter only derives and
52
+ returns — no mutating other state, no async, no DOM. Side effects *in reaction
53
+ to* a change are what `watch` is for.
54
+ - **A computed value is a read-only snapshot.** Never mutate what a `computed`
55
+ returns; update the *source* state it derives from and let a new snapshot be
56
+ produced.
57
+
58
+ ## When `watch` IS the right tool
59
+
60
+ Only when the effect crosses **out of** the reactive graph:
61
+
62
+ - **Fetch on a changing key** — prefer a reactive `useFetch` `query`/key (it
63
+ auto-refetches; see `nuxt-nitro-api/fetch-patterns.md`). Reach for `watch` only
64
+ for imperative `$fetch` keyed off a single id.
65
+ - **Drive a non-Vue library** — Tiptap, a Leaflet/Google map, a signature canvas,
66
+ a PrimeVue popover you `.hide()` imperatively.
67
+ - **DOM / timers** — `document.body.style.overflow`, a debounce `setTimeout`, a
68
+ polling `setInterval` (clear it in `onUnmounted`).
69
+ - **Persist** — a `localStorage`/`useCookie` write, debounced auto-save of a
70
+ deep-watched form.
71
+ - **URL sync** — `router.replace({ query: { ...route.query, tab } })`.
72
+ - **Re-seed local state on dialog open** — `watch(visible, (v) => { if (v) initForm() })`.
73
+ The single most common legit pattern.
74
+ - **Clone a server prop into a locally-editable draft** —
75
+ `watch(() => props.record, (r) => { if (r) form.value = structuredClone(toRaw(r)) }, { immediate: true })`.
76
+
77
+ ### Cancel stale work with `onWatcherCleanup`
78
+
79
+ A watcher that starts async work (a keyed `$fetch`, a timer) must cancel the
80
+ previous run, or out-of-order responses clobber state — the #1 correctness bug in
81
+ keyed-fetch watchers. Vue 3.5's `onWatcherCleanup(fn)` registers teardown that
82
+ runs before the next fire and on stop:
83
+
84
+ ```ts
85
+ import { onWatcherCleanup } from 'vue'
86
+
87
+ watch(query, async (q) => {
88
+ const ctrl = new AbortController()
89
+ onWatcherCleanup(() => ctrl.abort()) // abort the in-flight request if q changes again
90
+ results.value = await $fetch('/api/search', { query: { q }, signal: ctrl.signal })
91
+ })
92
+ ```
93
+
94
+ Gotcha: `onWatcherCleanup` only registers **synchronously, before the first
95
+ `await`**. For teardown decided after an await, use the callback's third arg
96
+ instead — `watch(src, (v, _old, onCleanup) => { … onCleanup(fn) })`.
97
+
98
+ ### `flush: 'post'` runs the callback after the DOM patches
99
+
100
+ When a watcher reacts to a change by reading/scrolling the **updated** DOM, give
101
+ it `{ flush: 'post' }` so it runs after Vue patches — no manual `await nextTick()`.
102
+ They're ~one microtask apart and interchangeable; pick by clarity (prefer
103
+ `flush: 'post'` for an effect that *reacts to reactive change*, `nextTick` for a
104
+ one-shot after an imperative toggle).
105
+
106
+ ```ts
107
+ watch(activeId, () => scrollActiveIntoView(), { flush: 'post' }) // DOM already updated
108
+ ```
109
+
110
+ ## Smell catalog (with refactors)
111
+
112
+ | Smell | Tell | Reach for |
113
+ |---|---|---|
114
+ | **derive-state** | body assigns a filtered/mapped view into another ref | `computed` |
115
+ | **prop-sync** | local `ref(props.x)` kept in sync by a watch (often + a 2nd watch emitting back) | `defineModel` or a writable `computed` |
116
+ | **side-effect-in-handler** | watching a value that only changes via one control, to clear a dependent field | that control's `@update:model-value` handler |
117
+ | **manual-refetch** | watching filter refs to call a function that calls `useFetch` | a `computed` `query` passed to `useFetch` |
118
+
119
+ The biggest real cluster was **side-effect-in-handler** — resetting dependent
120
+ fields when a Select changed. In a watcher it hides cause/effect and re-fires on
121
+ programmatic form reseeds; the colocated handler is direct and only fires on the
122
+ user action:
123
+
124
+ ```ts
125
+ // ❌ watch(() => form.entityType, () => { form.eventType = ''; form.conditionValue = null })
126
+ // ✅
127
+ function onEntityType(v) { form.entityType = v; form.eventType = ''; form.conditionValue = null }
128
+ // <Select v-model="form.entityType" @update:model-value="onEntityType" />
129
+ ```
130
+
131
+ Watch for **cascades** — a chain of watches each resetting the next (entityType →
132
+ eventType → conditionProperty) is just a chain of handlers, far harder to follow.
133
+
134
+ **Borderline tell — `seed-via-immediate`:** `watch(asyncList, (l) => { if (!local.value) local.value = pick(l) }, { immediate: true })` to default a *user-mutable* ref from fetched data is defensible (a pure `computed` can't be user-overridden) — but keep the `!local.value` guard so a refetch doesn't clobber an in-progress edit.
135
+
136
+ ## `deep: true` is expensive — use it sparingly
137
+
138
+ A deep watch traverses **every** nested property of the watched object on each
139
+ check, so it gets costly on large structures. Before reaching for it:
140
+
141
+ - **Watch a getter of the specific field** instead of the whole object:
142
+ `watch(() => form.address.zip, ...)` rather than `watch(form, ..., { deep: true })`.
143
+ - If you genuinely need to react to *any* field of a form (e.g. debounced
144
+ auto-save), a deep watch is legitimate — but scope it as tightly as you can.
145
+ - For deriving from a few keys of a nested object, a `computed` (or `watchEffect`)
146
+ tracks **only the keys actually read**, avoiding the full traversal a deep watch
147
+ pays for.
148
+
149
+ **Deep-watch aliasing trap:** in a deep watch the callback's `oldValue` and
150
+ `newValue` are the **same reference** (Vue mutated the object in place), so you
151
+ *cannot* diff old-vs-new inside the callback — `if (next.x !== prev.x)` is always
152
+ false. Watch a derived getter instead, so old/new are distinct primitives:
153
+
154
+ ```ts
155
+ // ❌ watch(items, (next, prev) => { if (next.length !== prev.length) … }, { deep: true }) // never fires
156
+ // ✅ watch(() => items.value.length, (next, prev) => { if (next > prev) onAdded() })
157
+ ```
158
+
159
+ ## `watch` vs `watchEffect`
160
+
161
+ Both run side effects reactively; they differ in how they **track dependencies**:
162
+
163
+ - **`watch(source, cb)`** tracks ONLY the explicit source, fires lazily and only
164
+ when it *actually changed*, and hands you **old + new** values. Use it when you
165
+ want precise control over what fires the effect, need the previous value, or
166
+ want it lazy.
167
+ - **`watchEffect(cb)`** runs immediately and auto-tracks every reactive property
168
+ read during its **synchronous** execution. Terser, and it removes the burden of
169
+ maintaining a source list — genuinely better for a side effect with **several**
170
+ dependencies, or one reading a few keys of a nested object (it tracks only
171
+ what's used, unlike a `deep` watch).
172
+
173
+ ```ts
174
+ // watch: todoId named twice (source + inside callback)
175
+ watch(todoId, async () => { data.value = await $fetch(`/api/todos/${todoId.value}`) }, { immediate: true })
176
+ // watchEffect: todoId.value is both the read and the tracked dep — no immediate flag, no repeated source
177
+ watchEffect(async () => { data.value = await $fetch(`/api/todos/${todoId.value}`) })
178
+ ```
179
+
180
+ Two cautions on `watchEffect`:
181
+
182
+ - **Async tracking gotcha:** it only tracks deps accessed **before the first
183
+ `await`**. Read a ref *after* an `await` and it silently won't re-trigger on
184
+ that ref. With multiple async deps, read them all up front or use an explicit
185
+ `watch`.
186
+ - **It tempts the derive-state smell.** Because it runs immediately and
187
+ auto-tracks, `watchEffect` is the most common home for "compute and assign" —
188
+ which is a `computed`. The *only* `watchEffect` smell in the audit was exactly
189
+ this: `watchEffect(() => { orgField.options = organizations.value })` → should
190
+ be a `computed` that maps the field list.
191
+
192
+ Default: **`computed` for derivation, `watch` for a precise single-source effect,
193
+ `watchEffect` for a genuine multi-dependency side effect** whose deps are all read
194
+ synchronously.