@gallopsystems/agent-skills 1.7.0 → 1.9.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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@gallopsystems/agent-skills",
3
- "version": "1.7.0",
3
+ "version": "1.9.0",
4
4
  "description": "Gallop Systems Claude Code skills, symlinked into .claude/skills on install.",
5
5
  "license": "UNLICENSED",
6
6
  "repository": {
@@ -62,6 +62,20 @@ git ls-remote --tags --refs --sort=-v:refname <template-url> 'v*' | head -1
62
62
 
63
63
  If newer, it pushes a **static branch name** (e.g. `chore/template-update`) with an `--allow-empty` commit and opens a PR whose body contains the version delta, release-notes/compare links, and step-by-step instructions an agent can execute. Hard-won details to keep if reimplementing: an explicit `permissions: contents: write, pull-requests: write` block (default token can't open PRs), a static branch name (dated branches caused duplicate PRs), and comparing **tag versions, not commit SHAs**.
64
64
 
65
+ ## Branch Protection in Descendants
66
+
67
+ A template **cannot** enable branch protection for the repos it generates — GitHub reads required status checks from repo config, never from committed workflow files. So every descendant starts with nothing gating merges until someone sets it once (after the first CI run, so the check is known). This template's CI exposes a **`ci-success`** summary job to be exactly that gate — require it on `main`:
68
+
69
+ ```bash
70
+ echo '{"required_status_checks":{"strict":false,"contexts":["ci-success"]},"enforce_admins":false,"required_pull_request_reviews":null,"restrictions":null}' \
71
+ | gh api -X PUT repos/<owner>/<repo>/branches/main/protection --input -
72
+ ```
73
+
74
+ - **Require the single `ci-success` context, not individual job names.** CI shards the test suite, so the per-leg check names embed the matrix size (`test (shard 1/4)`) and change as the project grows — protection pinned to them blocks every PR the moment the count shifts. `ci-success` is a summary job (`needs:` all gating jobs, `if: always()`, fails unless every `needs.*.result` is `success`); `needs.test.result` rolls the whole matrix into one value, so it stays correct at any shard count. If you add a template whose CI lacks such a job, create one rather than requiring the matrix legs directly.
75
+ - **`coverage` is deliberately excluded** from `ci-success` so the report never blocks a merge — don't add it to the required contexts.
76
+ - The PUT body must include `required_status_checks`, `enforce_admins`, `required_pull_request_reviews`, and `restrictions` (any may be `null`) or the call 422s. Needs admin on the repo.
77
+ - A Conventional-Commits **PR-title check is only worth requiring on repos something actually reads the title** — i.e. a published package with release-please/changelog automation. A private app (no release tooling, `"private": true`, no version) gains nothing from it; don't gate on it there.
78
+
65
79
  ## Further Reading
66
80
 
67
81
  - **Template anatomy & testing changes**: [template-authoring.md](template-authoring.md)
@@ -76,7 +76,7 @@ From nuxt-auth-utils:
76
76
  - `hashPassword`, `verifyPassword`
77
77
  - `defineOAuth*EventHandler` (Google, GitHub, etc.)
78
78
 
79
- **Need to import:** `z` from "zod", `fromZodError` from "zod-validation-error"
79
+ **Need to import:** `z` from "zod" (zod 4 — error formatting is built in via `z.prettifyError()`; no `zod-validation-error` needed)
80
80
 
81
81
  ### Client-side
82
82
 
@@ -104,7 +104,7 @@ const query = await getValidatedQuery(event, z.object({
104
104
  }));
105
105
 
106
106
  const body = await readValidatedBody(event, z.object({
107
- email: z.string().email(),
107
+ email: z.email(), // Zod 4: top-level, not z.string().email()
108
108
  name: z.string().min(1),
109
109
  }));
110
110
  ```
@@ -33,7 +33,7 @@ Tasks live in `server/tasks/`. Directory structure = task name with colons:
33
33
  import { z } from "zod";
34
34
 
35
35
  const PayloadSchema = z.object({
36
- to: z.string().email(),
36
+ to: z.email(), // Zod 4: top-level, not z.string().email()
37
37
  subject: z.string(),
38
38
  body: z.string(),
39
39
  });
@@ -43,25 +43,28 @@ const query = await getValidatedQuery(event, (data) => querySchema.parse(data));
43
43
 
44
44
  ## Pattern 3: safeParse for Better Errors
45
45
 
46
+ Zod 4 has a built-in `z.prettifyError()` — no `zod-validation-error` dependency needed:
47
+
46
48
  ```typescript
47
- import { fromZodError } from "zod-validation-error";
49
+ import { z } from "zod";
48
50
 
49
51
  const rawQuery = getQuery(event);
50
52
  const result = querySchema.safeParse(rawQuery);
51
53
 
52
54
  if (!result.success) {
53
- console.error("Validation error:", result.error); // Dev log
54
- const userError = fromZodError(result.error); // User-friendly
55
+ console.error("Validation error:", z.treeifyError(result.error)); // structured dev log
55
56
  throw createError({
56
57
  statusCode: 400,
57
58
  statusMessage: "Bad Request",
58
- message: userError.message,
59
+ message: z.prettifyError(result.error), // human-readable, e.g. "✖ Invalid email address\n → at email"
59
60
  });
60
61
  }
61
62
 
62
63
  return result.data;
63
64
  ```
64
65
 
66
+ `z.prettifyError(err)` returns a readable multi-line string; `z.treeifyError(err)` returns a nested object keyed by field (the replacement for the deprecated `.format()`). Use `z.flattenError(err)` for a flat `{ formErrors, fieldErrors }` shape.
67
+
65
68
  ## Common Zod Patterns
66
69
 
67
70
  ### Query Parameters
@@ -90,7 +93,7 @@ const querySchema = z.object({
90
93
 
91
94
  ```typescript
92
95
  const createUserSchema = z.object({
93
- email: z.string().email(),
96
+ email: z.email(), // Zod 4: top-level, NOT z.string().email()
94
97
  name: z.string().min(1).max(100),
95
98
  role: z.enum(["admin", "user"]).default("user"),
96
99
  metadata: z.record(z.string(), z.any()).optional(),
@@ -117,7 +120,7 @@ Export schemas for client-side type reuse:
117
120
  import { z } from "zod";
118
121
 
119
122
  export const CreateUserSchema = z.object({
120
- email: z.string().email(),
123
+ email: z.email(),
121
124
  name: z.string().min(1),
122
125
  });
123
126
 
@@ -129,3 +132,17 @@ const body: CreateUserInput = { email: "test@example.com", name: "Test" };
129
132
  ```
130
133
 
131
134
  **Note:** Nitro auto-generates response types, but NOT input types from Zod schemas.
135
+
136
+ ## Zod 4 Notes (this stack pins zod ^4)
137
+
138
+ The model's prior is mostly Zod 3 — these are the idioms that changed. Get them right.
139
+
140
+ - **String formats are top-level functions, not `z.string()` methods.** Use `z.email()`, `z.url()`, `z.uuid()`, `z.ipv4()`, `z.iso.datetime()`. The chained forms (`z.string().email()`) are deprecated.
141
+ - **Error customization is one `error` param.** `message`, `invalid_type_error`, `required_error`, and `errorMap` are gone:
142
+ ```typescript
143
+ z.string().min(5, { error: "Too short." });
144
+ z.string({ error: (iss) => iss.input === undefined ? "Required" : "Must be a string" });
145
+ ```
146
+ - **Format errors with the built-ins**, not `zod-validation-error`: `z.prettifyError()` (human string), `z.treeifyError()` (nested, replaces deprecated `.format()`), `z.flattenError()` (replaces deprecated `.flatten()`).
147
+ - **`.default()` applies to the *output* type** and short-circuits parsing when input is `undefined`. For the old "run the default through the schema" behavior, use `.prefault()`.
148
+ - **`z.coerce.*` input type is now `unknown`** (not the output type) — fine for h3 query/body parsing, but affects schemas you consume elsewhere.
@@ -0,0 +1,8 @@
1
+ {
2
+ "name": "tailwind-v4",
3
+ "description": "Tailwind CSS v4 (CSS-first): @import/@theme/@source config, the no-tailwind.config.js model, prefers-color-scheme dark mode, and the v3->v4 traps an LLM trained on v3 falls into",
4
+ "version": "1.0.0",
5
+ "author": {
6
+ "name": "yeedle"
7
+ }
8
+ }
@@ -0,0 +1,133 @@
1
+ ---
2
+ name: tailwind-v4
3
+ description: Work effectively with Tailwind CSS v4 (the CSS-first model) in a Nuxt/Vite + PrimeVue/Volt stack. Use when editing Tailwind CSS, configuring @theme/@source/@import, setting up dark mode, defining design tokens, or debugging classes that silently don't apply. Covers the v3→v4 traps an LLM trained on v3 falls into.
4
+ ---
5
+
6
+ # Tailwind CSS v4
7
+
8
+ Tailwind v4 is **CSS-first**: configuration lives in your CSS file, not a
9
+ `tailwind.config.js`. Most day-to-day utility usage is unchanged and the model is
10
+ already good at it — this skill is about the **v4-specific shifts** and the traps
11
+ that come from a v3-trained prior. When a class "isn't working," the cause is
12
+ almost always one of the gotchas below.
13
+
14
+ ## When to use this skill
15
+
16
+ - Editing the Tailwind CSS entry file (`@import`, `@theme`, `@source`, `@custom-variant`)
17
+ - Defining or renaming design tokens / colors
18
+ - Setting up or debugging dark mode
19
+ - A utility class silently renders nothing and you don't know why
20
+ - Migrating or porting v3 code (anything with `tailwind.config.js`, `@tailwind`, `darkMode: 'class'`)
21
+
22
+ ## The CSS-first setup (no JS config)
23
+
24
+ There is **no `tailwind.config.js`/`.ts`**. The whole config is in your CSS entry
25
+ (e.g. `app/assets/css/main.css`):
26
+
27
+ ```css
28
+ @import "tailwindcss"; /* NOT @tailwind base/components/utilities */
29
+ @import "tailwindcss-primeui";
30
+ @source "../../../src/volt"; /* scan sources Tailwind won't auto-detect */
31
+
32
+ @theme {
33
+ --color-surface: #ffffff; /* defines the bg-surface / text-surface utilities */
34
+ --color-fg: #18181b;
35
+ }
36
+ ```
37
+
38
+ Build integration is the **Vite plugin**, not the old Nuxt module or PostCSS:
39
+
40
+ ```ts
41
+ // nuxt.config.ts / vite.config.ts
42
+ import tailwindcss from "@tailwindcss/vite";
43
+ export default defineNuxtConfig({ vite: { plugins: [tailwindcss()] } });
44
+ ```
45
+
46
+ Do **not** add `@nuxtjs/tailwindcss`, a `postcss.config.js` with `tailwindcss: {}`,
47
+ or `@tailwind base/components/utilities` — those are v3 and either no-op or break.
48
+ `tailwindcss` itself must be a direct dependency (alongside `@tailwindcss/vite`),
49
+ or `tailwindcss-primeui` raises peer-dependency warnings.
50
+
51
+ ## Design tokens: `@theme` vs `:root`
52
+
53
+ - A custom color token must live in **`@theme`** to generate a utility:
54
+ `@theme { --color-canvas: #fafafa }` → `bg-canvas`/`text-canvas` exist.
55
+ The same variable in `:root` is just a CSS variable — **no utility class**.
56
+ - **The utility name is the full `--color-*` suffix.** `--color-fg-subtle` →
57
+ `text-fg-subtle`. A shortened or undefined name (`text-subtle`, or a dangling
58
+ `bg-surface-strong` with no matching token) **generates nothing, with no error**
59
+ — the element just renders unstyled. First thing to check when a custom color
60
+ "does nothing": does the token name match exactly?
61
+ - **Inverse for PrimeUI:** PrimeVue's `--p-*` variables stay in `:root`. The
62
+ `tailwindcss-primeui` plugin turns those into utilities — don't move them into
63
+ `@theme`.
64
+
65
+ ## `@source` — v4 only generates classes from scanned files
66
+
67
+ v4 auto-detects content, but skips files outside the project's default roots (and
68
+ anything gitignored). Classes used **only** inside a vendored/library directory
69
+ (e.g. Volt components in `src/volt`) get purged and silently go missing. Point
70
+ Tailwind at them: `@source "../../../src/volt";`. If a vendored component's
71
+ classes aren't generating, check this line first.
72
+
73
+ ## Dark mode
74
+
75
+ v4's `dark:` variant defaults to **`@media (prefers-color-scheme: dark)`** (OS
76
+ preference) — there is no `darkMode: 'class'` config and no `.dark` class by
77
+ default. Two consequences:
78
+
79
+ 1. **Prefer semantic tokens over `dark:` pairs.** Writing `bg-white dark:bg-zinc-900`
80
+ on every element means one forgotten half silently breaks dark mode. Instead
81
+ define a token that *flips its value* in one media block, and use it with no
82
+ `dark:`:
83
+ ```css
84
+ @theme { --color-surface: #fff; --color-fg: #18181b; }
85
+ @media (prefers-color-scheme: dark) {
86
+ :root { --color-surface: #18181b; --color-fg: #f4f4f5; }
87
+ }
88
+ ```
89
+ ```html
90
+ <div class="bg-surface text-fg"><!-- flips automatically, no dark: --></div>
91
+ ```
92
+ 2. **For a manual (class/toggle) dark mode, you must redefine the variant** —
93
+ `darkMode: 'class'` is gone:
94
+ ```css
95
+ @custom-variant dark (&:where(.p-dark, .p-dark *));
96
+ ```
97
+ Because the default keys off the OS, a JS toggle must also override the tokens
98
+ (e.g. via a `[data-theme]` block), and any non-CSS consumer of the scheme
99
+ (canvas/chart libs — see gotchas) must be re-notified on toggle.
100
+
101
+ ## More gotchas
102
+
103
+ The full set mined from real debugging sessions — `@apply` in scoped `<style>`,
104
+ JS-driven colors that can't read tokens, `@import url()` ordering, the PrimeUI
105
+ surface ramp vs flipping tokens, and more — is in
106
+ [gotchas.md](gotchas.md). Read it when touching CSS in this stack.
107
+
108
+ ## Optional tooling
109
+
110
+ The community **[`tailwindcss-mcp-server`](https://github.com/clarity-contrib/tailwindcss-mcp-server)**
111
+ (`npx -y tailwindcss-mcp-server`) exposes utility/color lookup, docs search, and
112
+ CSS→Tailwind conversion, with a `version` param defaulting to `v4`. Useful for
113
+ class discovery, but it does not encode the stack-specific gotchas above.
114
+
115
+ ## Related
116
+
117
+ - **PrimeVue/Volt component styling** (`pt:` pass-through, `ptViewMerge`,
118
+ `data-pc-*`, `@apply` + `@reference` in SFC `<style>`, reactive chart palettes):
119
+ see the **volt-primevue** skill — it owns the component layer; this skill owns
120
+ the Tailwind-v4 fundamentals.
121
+
122
+ ## Contributing Back
123
+
124
+ This skill grows by capturing what it missed. If you just worked through something
125
+ in this domain that this skill did not cover — an error you had to figure out, a
126
+ behavior that contradicts what is documented above, a workflow knot — ask the
127
+ user: **"Want me to contribute this back to the tailwind-v4 skill?"**
128
+
129
+ If yes, run `/contribute-skill`. If that command is not available, do the
130
+ equivalent inline: distill the generic lesson (placeholders only — no project
131
+ names, IDs, domains, or secrets), then branch or fork
132
+ [gallop-systems/agent-skills](https://github.com/gallop-systems/agent-skills) and
133
+ open a PR editing this skill.
@@ -0,0 +1,84 @@
1
+ # Tailwind v4 Gotchas
2
+
3
+ Mined from real debugging sessions in this stack (Nuxt/Vite + Tailwind v4 +
4
+ PrimeVue/Volt). Each is a place where a class silently did nothing, a v3 habit
5
+ broke, or the fix was non-obvious.
6
+
7
+ ## `@apply` of custom tokens in a `<style>` block needs `@reference` to YOUR css
8
+
9
+ In v4, a component-scoped `<style>` doesn't see your Tailwind context. `@apply`
10
+ there needs a `@reference` — and the trap is that `@reference "tailwindcss"` loads
11
+ only the **default** theme, so built-in utilities (`rounded-lg`, `zinc-*`) work
12
+ but your custom `@theme` tokens fail with *"Cannot apply unknown utility class."*
13
+
14
+ ```css
15
+ /* ❌ @reference "tailwindcss"; → @apply text-fg fails */
16
+ @reference "../assets/css/main.css"; /* ✅ exposes YOUR tokens; path is relative to the file */
17
+ .prose :where(h2) { @apply text-fg; }
18
+ ```
19
+
20
+ Better still in scoped styles: skip `@apply` and consume the generated CSS
21
+ variables directly — `color: var(--color-fg)` always works with no `@reference`.
22
+
23
+ Note: a full `yarn build` validates `@apply` in `<style>`; a bare
24
+ `@tailwindcss/cli` compile does **not** — the CLI can pass while the real build
25
+ fails. Include `build` in your check. (The SFC-specific details live in the
26
+ **volt-primevue** skill's gotchas.)
27
+
28
+ ## `@import url(...)` must come before `@import "tailwindcss"`
29
+
30
+ The CSS spec requires all `@import` statements to precede other rules. Because
31
+ `@import "tailwindcss"` inlines real rules, any plain `@import url(...)` (e.g.
32
+ Google Fonts) placed after it is invalid and silently dropped.
33
+
34
+ ```css
35
+ @import url("https://fonts.googleapis.com/css2?family=Inter"); /* ✅ first */
36
+ @import "tailwindcss";
37
+ @import "some-lib/dist/style.css";
38
+ ```
39
+
40
+ ## JS-set colors can't read `var(--color-*)` — they won't flip with the theme
41
+
42
+ Anything that takes colors as JS values (ApexCharts, canvas, SVG attributes
43
+ written from script) can't use `bg-surface`/`var(--color-fg)` — the value is baked
44
+ at render and won't follow `prefers-color-scheme`. A `colors: ["#18181b"]`
45
+ (near-black) chart line is invisible on dark. Compute a palette from a
46
+ `window.matchMedia("(prefers-color-scheme: dark)")` match and recompute on its
47
+ `change` event. (The volt-primevue skill has the Vue composable version.)
48
+
49
+ ## Two surface systems: PrimeUI's ramp is fixed, your `@theme` tokens flip
50
+
51
+ - PrimeUI's `--p-surface-0 … --p-surface-950` ramp is defined **once** and does
52
+ **not** change with the theme. `bg-surface-0` is always white — to go dark you
53
+ need an explicit `dark:bg-surface-900` pair. Vendored Volt components rely on
54
+ this full 0–950 ramp.
55
+ - Your `@theme` semantic tokens (`--color-surface`, `--color-fg`) **flip their
56
+ value** in the dark `@media` block, so `bg-surface` needs no `dark:`.
57
+
58
+ Rule: tokenize only the markup you own (pages, your components). Leave vendored
59
+ Volt components on the `surface-*` ramp + `dark:` pairs so they stay
60
+ upstream-compatible. Mixing the two is what produces white-striped tables on dark.
61
+
62
+ ## Don't port a v3 `tailwind.config.js` into a v4 app
63
+
64
+ None of it applies, and some of it actively breaks:
65
+
66
+ ```js
67
+ // ❌ v3 — delete the whole file in v4
68
+ module.exports = {
69
+ content: ["./**/*.vue"], // → automatic detection (+ @source for out-of-tree)
70
+ darkMode: "class", // → @custom-variant dark (...) in CSS, or rely on @media
71
+ theme: { extend: { colors: {...} } }, // → @theme { --color-* } in CSS
72
+ plugins: [require("...")], // → @plugin / @import in CSS; require() also breaks in ESM
73
+ };
74
+ ```
75
+
76
+ Also drop the `@nuxtjs/tailwindcss` module — it's v3-era and has been observed to
77
+ trigger an infinite dev-server regeneration loop. Use `@tailwindcss/vite`.
78
+
79
+ ## Benign noise
80
+
81
+ Under Yarn (esp. PnP), `@tailwindcss/vite` and `tailwindcss-primeui` may emit
82
+ "doesn't provide vite/tailwindcss" unmet-peer-dependency warnings even though the
83
+ deps resolve transitively. These are safe to ignore — they are not a config bug.
84
+ (Adding `tailwindcss` as a direct dependency silences the `tailwindcss-primeui` one.)
@@ -106,6 +106,31 @@ section name is prefixed **`pc`** (e.g. a Badge embedded in another component is
106
106
  <VoltSomething pt:pcBadge:root:class="bg-red-500" />
107
107
  ```
108
108
 
109
+ ## State styling: use `tailwindcss-primeui`'s `p-*` variants, not edited source
110
+
111
+ `tailwindcss-primeui` registers component-state variants — `p-selected:`,
112
+ `p-focus:`, `p-disabled:`, `p-invalid:`, `p-editable:` — that key off PrimeVue's
113
+ internal state. Reach for these via `pt:` instead of editing vendored component
114
+ source to style a state:
115
+
116
+ ```vue
117
+ <VoltSelect pt:option:class="p-selected:bg-highlight p-focus:bg-surface-100" />
118
+ ```
119
+
120
+ ## PrimeIcons `.pi` is `display:inline-block` — it beats `hidden`
121
+
122
+ PrimeIcons sets `display: inline-block` on `.pi`, which has higher specificity
123
+ than Tailwind's `hidden` (`display:none`). Toggling an icon's visibility with bare
124
+ `hidden`/`inline-block` silently fails — the icon stays visible. Use the `!`
125
+ important variant:
126
+
127
+ ```vue
128
+ <!-- ❌ stays visible -->
129
+ <i class="pi pi-x hidden sm:inline-block" />
130
+ <!-- ✅ -->
131
+ <i class="pi pi-x !hidden sm:!inline-block" />
132
+ ```
133
+
109
134
  ## We don't use `@primevue/forms`
110
135
 
111
136
  This stack validates with **zod + manual wiring**, not `@primevue/forms`. The
@@ -1,6 +1,6 @@
1
1
  ---
2
2
  name: vue-nuxt
3
- description: Author Vue 3 components inside a Nuxt 4 app. Covers Nuxt auto-import rules, component authoring (props/emits/withDefaults/generics), v-model/defineModel, slots, composables, reactivity, when watch is a code smell, page structure, display formatting, and Vue-shaped template idioms.
3
+ description: Author Vue 3 components inside a Nuxt 4 app. Covers Nuxt auto-import rules, component authoring (props/emits/withDefaults/generics), v-model/defineModel, slots, composables, reactivity, when watch is a code smell, reaching for VueUse over hand-rolled effects, page structure, display formatting, and Vue-shaped template idioms.
4
4
  ---
5
5
 
6
6
  # Vue-in-Nuxt component authoring
@@ -23,6 +23,7 @@ cross-links to those rather than restating them.
23
23
  - Formatting display values (currency/date/number) consistently
24
24
  - Anything reactivity-shaped: `computed` vs `watch`, prop→state sync, DOM measurement
25
25
  - You see `watch` and want to know if it should be something else
26
+ - About to hand-roll a `watch` + lifecycle teardown for a browser API (storage, timers, DOM, listeners, URL) — VueUse likely wraps it
26
27
 
27
28
  ## Reference Files
28
29
 
@@ -33,6 +34,7 @@ cross-links to those rather than restating them.
33
34
  - [composables.md](./composables.md) — `MaybeRefOrGetter`/`toValue` argument contract, return refs not `reactive()`, thin pure-core shell, `onScopeDispose`/`effectScope` cleanup
34
35
  - [reactivity.md](./reactivity.md) — `ref` over `reactive`, `useTemplateRef`, pure computeds, mutate-don't-reassign, DOM-measure + `ResizeObserver`, `shallowRef`, watch-getter prop sync, `:key` remount, listener cleanup
35
36
  - [watch.md](./watch.md) — **`watch` is the escape hatch, not the default**: when it's right, and the four smell shapes (with refactors) found auditing 159 real watchers
37
+ - [vueuse.md](./vueuse.md) — reach for a VueUse composable before hand-rolling a `watch` + lifecycle teardown for an external-world effect (DOM, timers, storage, URL, listeners); the `@vueuse/nuxt` setup, the watch-sugar functions, and what to keep on Nuxt's own APIs
36
38
  - [template-idioms.md](./template-idioms.md) — duplicate-`@keyup` TS error, `:deep()`/`:slotted()`/`:global()`, click-outside marker class, `NuxtLink`/thin `app.vue`, `useHead`, `v-bind` shorthand, `useId`, `<Teleport>`/`<KeepAlive>`, `v-memo`/`v-once`, file-input reset
37
39
  - [page-structure.md](./page-structure.md) — keep pages thin: route-param parsing + layout in the page, data/logic/forms in components
38
40
  - [formatters.md](./formatters.md) — never inline a currency/date/number formatter; centralize in `useFormatters`, prefer Intl/date-fns
@@ -42,7 +44,7 @@ cross-links to those rather than restating them.
42
44
  1. **Lean on auto-imports.** `app/components`, `app/composables`, `app/utils`, and the Vue/Nuxt APIs all auto-import. Add an explicit `import` only for third-party symbols and TS types. A nested component's tag carries its directory as a prefix (`components/customers/ProfileCard.vue` → `<CustomersProfileCard>`).
43
45
  2. **Type props/emits, default the booleans.** Use the type-only macros (`defineProps<{...}>()`, `defineEmits<{...}>()`). A bare `boolean` prop coerces to `false` when absent (not `undefined`), so any "defaults-on" flag MUST be defaulted — via reactive destructure (`{ flag = true } = defineProps<…>()`, the 3.5 default, no factory needed for arrays/objects) or `withDefaults` (factory required for non-primitives).
44
46
  3. **`computed` for derivation, `watch` for escaping the graph.** If a watcher body just assigns one reactive value from others, it's a `computed`. Need to write a value back? A `computed` can have a setter — reach for a writable `computed` or `defineModel` before a sync watcher. Keep computed getters pure (no fetch, no mutation, no DOM).
45
- 4. **Tie effects to lifecycle.** DOM measurement, listeners, observers, and timers go in `onMounted` and are torn down in `onUnmounted`. A computed reading live DOM geometry needs an explicit re-measure signal (DOM size isn't reactive).
47
+ 4. **Tie effects to lifecycle.** DOM measurement, listeners, observers, and timers go in `onMounted` and are torn down in `onUnmounted`. A computed reading live DOM geometry needs an explicit re-measure signal (DOM size isn't reactive). Before hand-rolling that effect-plus-teardown, check whether a **VueUse** composable already wraps it (`vueuse.md`) — they bundle the cleanup.
46
48
  5. **Call composables at the top of `<script setup>`** — never inside a callback or a template expression (both lose Nuxt's request scope). Derive display state with `computed`, guarding for possibly-null data.
47
49
  6. **Defer to the right skill.** Fetch/SSR/auth/middleware → `nuxt-nitro-api`. Volt components, `pt:` styling, color tokens, dark mode → `volt-primevue`. Don't duplicate them here.
48
50
 
@@ -0,0 +1,75 @@
1
+ # VueUse — reach for a composable before hand-rolling an effect
2
+
3
+ `watch.md` lists the cases where `watch` is legit: they're all effects that cross
4
+ **out of** the reactive graph (DOM, timers, storage, URL, the network). For most
5
+ of those, **VueUse already wraps the boundary** — and a VueUse composable bundles
6
+ the *teardown* with the reactive effect, so it retires the manual-`onUnmounted`
7
+ cleanup, not just the watch. Before you write a `watch` + `onMounted`/`onUnmounted`
8
+ pair against a browser API, check whether a composable does it.
9
+
10
+ > Not a replacement for the framework. Data fetching stays on
11
+ > `useFetch`/`useAsyncData` (see `nuxt-nitro-api`); cookies use Nuxt's built-in
12
+ > `useCookie`; derivation stays a `computed`. VueUse is for the *external-world*
13
+ > effects — see the caveats at the bottom.
14
+
15
+ ## Setup in Nuxt
16
+
17
+ Add the module; it auto-imports every `@vueuse/core` function, so they need no
18
+ explicit `import` (same as Nuxt's own composables — see `auto-imports.md`):
19
+
20
+ ```ts
21
+ // nuxt.config.ts
22
+ export default defineNuxtConfig({ modules: ['@vueuse/nuxt'] })
23
+ ```
24
+
25
+ The **`@vueuse/router`** and **`@vueuse/integrations`** add-ons are separate
26
+ installs and are **not** auto-imported by the module — import them explicitly.
27
+
28
+ ## The boundary cases (mapped from `watch.md`)
29
+
30
+ | You were about to `watch` for… | Reach for | Why it's better |
31
+ |---|---|---|
32
+ | `document.body.style.overflow` toggle | `useScrollLock(document.body)` | writable ref; restores the prior value on unmount |
33
+ | `setInterval` polling | `useIntervalFn(fn, ms)` / `useTimeoutPoll` | **auto-clears on unmount** — no `clearInterval` to forget |
34
+ | `setTimeout` | `useTimeoutFn(fn, ms)` | same auto-cleanup, with `start`/`stop` controls |
35
+ | debounced auto-save | `useDebounceFn` / `refDebounced` | no manual timer ref + cleanup |
36
+ | `localStorage`/`sessionStorage` write-back | `useLocalStorage(key, init)` / `useStorage` | a reactive ref mirrored to storage both ways — the write-back watch disappears |
37
+ | `addEventListener` in `onMounted` | `useEventListener(target, evt, fn)` | auto-removes on unmount |
38
+ | click-outside marker class | `onClickOutside(el, fn)` | replaces the hand-rolled document listener (cf. `template-idioms.md`) |
39
+ | `ResizeObserver` re-measure | `useElementSize` / `useElementBounding` / `useResizeObserver` | reactive size, observer torn down for you (cf. `reactivity.md`) |
40
+ | `matchMedia` listener | `useMediaQuery` / `usePreferredColorScheme` / `usePreferredDark` | reactive boolean, no listener bookkeeping |
41
+ | clipboard write + "copied!" flag | `useClipboard()` → `{ copy, copied }` | `copied` auto-resets |
42
+
43
+ URL sync (`router.replace({ query: { ...route.query, tab } })`) →
44
+ `useRouteQuery('tab')` from `@vueuse/router` gives a ref bound two-way to the query
45
+ param. Nuxt's own `useRoute()` is already reactive for *reads*; reach for
46
+ `useRouteQuery` when you want a **writable** ref bound to a single param.
47
+
48
+ ## VueUse's `watch` sugar — when a `watch` IS warranted
49
+
50
+ When the effect genuinely belongs in a watcher, VueUse's Watch category removes the
51
+ boilerplate `watch.md` warns about:
52
+
53
+ - **`watchDebounced(src, cb, { debounce: 300 })`** / **`watchThrottled`** — the
54
+ debounced-auto-save case without a `setTimeout` inside the callback.
55
+ - **`whenever(source, cb)`** — fires only when `source` becomes truthy; the
56
+ "re-seed on dialog open" pattern (`watch(visible, v => { if (v) … })`) as one line.
57
+ - **`until(source).toBe(x)`** — await a reactive condition instead of polling.
58
+ - **`watchOnce`** — auto-stops after the first fire; no manual `stop()` handle.
59
+ - **`watchIgnorable` / `watchPausable`** — suppress or pause a watcher around a
60
+ programmatic write (a cleaner answer than guard flags for the "re-fires on
61
+ programmatic reseed" trap in `watch.md`).
62
+
63
+ `onWatcherCleanup` and `flush: 'post'` (in `watch.md`) still apply — these are sugar
64
+ over the same watcher, not a different mechanism.
65
+
66
+ ## Caveats — don't over-reach
67
+
68
+ - **Cookies:** use Nuxt's built-in **`useCookie`** (SSR-aware), not VueUse's
69
+ `@vueuse/integrations` `useCookies` (a `universal-cookie` wrapper).
70
+ - **Fetching:** `useFetch`/`useAsyncData`/`$fetch` own the data layer
71
+ (`nuxt-nitro-api`). Skip VueUse's `useFetch`/`useAsyncState` in a Nuxt app.
72
+ - **Head/title:** prefer Nuxt's `useHead`/`useSeoMeta` over `useTitle`/`useFavicon`.
73
+ - **Derivation is still a `computed`.** VueUse doesn't change the core rule: if the
74
+ body just assigns one reactive value from others, it's a `computed`, not a
75
+ composable and not a watch.
@@ -74,6 +74,20 @@ Only when the effect crosses **out of** the reactive graph:
74
74
  - **Clone a server prop into a locally-editable draft** —
75
75
  `watch(() => props.record, (r) => { if (r) form.value = structuredClone(toRaw(r)) }, { immediate: true })`.
76
76
 
77
+ > **Before hand-rolling the external-world cases, check VueUse.** Most of the
78
+ > bullets above (DOM, timers, persist, URL) are exactly what VueUse wraps — and a
79
+ > VueUse composable bundles the *teardown* with the reactive effect, so it
80
+ > retires the manual-`onUnmounted` footgun, not just the watch:
81
+ >
82
+ > - **DOM / body scroll** — `useScrollLock(document.body)` over a watch toggling `document.body.style.overflow`.
83
+ > - **Timers** — `useIntervalFn` / `useTimeoutFn` / `useTimeoutPoll` auto-clear on unmount (no manual `clearInterval`); `useDebounceFn` / `refDebounced` for debounce.
84
+ > - **Persist** — `useLocalStorage` / `useStorage`: a reactive ref mirrored to storage, no write-back watch.
85
+ > - **URL sync** — `useRouteQuery` (from `@vueuse/router`) for a ref bound to a query param.
86
+ >
87
+ > Nuxt caveats: cookies use the built-in `useCookie`, **not** VueUse's
88
+ > `@vueuse/integrations` `useCookies`; data fetching stays on
89
+ > `useFetch`/`useAsyncData`. Auto-import the rest via the `@vueuse/nuxt` module.
90
+
77
91
  ### Cancel stale work with `onWatcherCleanup`
78
92
 
79
93
  A watcher that starts async work (a keyed `$fetch`, a timer) must cancel the