@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.
- package/README.md +2 -1
- package/package.json +1 -1
- package/plugins/nuxt-nitro-api/.claude-plugin/plugin.json +1 -1
- package/plugins/nuxt-nitro-api/skills/nuxt-nitro-api/SKILL.md +23 -8
- package/plugins/nuxt-nitro-api/skills/nuxt-nitro-api/auth-patterns.md +1 -0
- package/plugins/nuxt-nitro-api/skills/nuxt-nitro-api/caching.md +68 -0
- package/plugins/nuxt-nitro-api/skills/nuxt-nitro-api/composables-utils.md +31 -1
- package/plugins/nuxt-nitro-api/skills/nuxt-nitro-api/error-handling.md +74 -0
- package/plugins/nuxt-nitro-api/skills/nuxt-nitro-api/fetch-patterns.md +18 -0
- package/plugins/nuxt-nitro-api/skills/nuxt-nitro-api/formatters.md +11 -0
- package/plugins/nuxt-nitro-api/skills/nuxt-nitro-api/layers.md +40 -0
- package/plugins/nuxt-nitro-api/skills/nuxt-nitro-api/page-structure.md +1 -0
- package/plugins/nuxt-nitro-api/skills/nuxt-nitro-api/route-rules.md +41 -0
- package/plugins/nuxt-nitro-api/skills/nuxt-nitro-api/server-runtime.md +97 -0
- package/plugins/nuxt-nitro-api/skills/nuxt-nitro-api/ssr-client.md +60 -0
- package/plugins/nuxt-nitro-api/skills/nuxt-nitro-api/state-management.md +68 -0
- package/plugins/nuxt-nitro-api/skills/nuxt-nitro-api/storage.md +59 -0
- package/plugins/volt-primevue/skills/volt-primevue/SKILL.md +56 -12
- package/plugins/volt-primevue/skills/volt-primevue/config.md +51 -0
- package/plugins/volt-primevue/skills/volt-primevue/gotchas.md +49 -0
- package/plugins/volt-primevue/skills/volt-primevue/theming.md +20 -6
- package/plugins/vue-nuxt/.claude-plugin/plugin.json +8 -0
- package/plugins/vue-nuxt/skills/vue-nuxt/SKILL.md +48 -0
- package/plugins/vue-nuxt/skills/vue-nuxt/auto-imports.md +48 -0
- package/plugins/vue-nuxt/skills/vue-nuxt/component-authoring.md +159 -0
- package/plugins/vue-nuxt/skills/vue-nuxt/composables.md +95 -0
- package/plugins/vue-nuxt/skills/vue-nuxt/reactivity.md +133 -0
- package/plugins/vue-nuxt/skills/vue-nuxt/slots.md +139 -0
- package/plugins/vue-nuxt/skills/vue-nuxt/template-idioms.md +142 -0
- package/plugins/vue-nuxt/skills/vue-nuxt/v-model.md +106 -0
- package/plugins/vue-nuxt/skills/vue-nuxt/watch.md +194 -0
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
# Shared State with `useState`
|
|
2
|
+
|
|
3
|
+
Cross-component shared state in Nuxt is `useState` — NOT a module-scope `ref`.
|
|
4
|
+
This is the single most important state rule in an SSR app, because the wrong
|
|
5
|
+
version is a cross-request data leak, not just a bug.
|
|
6
|
+
|
|
7
|
+
## Never export a module-scope `ref`
|
|
8
|
+
|
|
9
|
+
A `ref` declared at module scope is created **once per server process** and shared
|
|
10
|
+
by every request the server handles. During SSR that means one user can see
|
|
11
|
+
another user's data. `useState(key, init)` creates state that is **per-request on
|
|
12
|
+
the server** and hydrated to the client:
|
|
13
|
+
|
|
14
|
+
```typescript
|
|
15
|
+
// ❌ SHARED ACROSS ALL SSR REQUESTS — leaks state between users
|
|
16
|
+
export const user = ref(null);
|
|
17
|
+
|
|
18
|
+
// ✅ keyed, per-request, hydration-safe — wrap it in a composable
|
|
19
|
+
export const useUser = () => useState("user", () => null);
|
|
20
|
+
```
|
|
21
|
+
|
|
22
|
+
Always expose `useState` through a `use*` composable so the key is defined once
|
|
23
|
+
and can't drift between call sites.
|
|
24
|
+
|
|
25
|
+
## Rules
|
|
26
|
+
|
|
27
|
+
- **Provide a factory initializer:** `useState("count", () => 0)`. The init runs
|
|
28
|
+
on the server; the value is serialized into the payload and reused on the
|
|
29
|
+
client (no re-init, no mismatch).
|
|
30
|
+
- **State must be JSON-serializable** — no class instances, functions, `Date`
|
|
31
|
+
round-trips, or `Map`/`Set`. It travels through the SSR payload as JSON.
|
|
32
|
+
- **Same key = same state.** Two `useState("user")` calls anywhere in the app
|
|
33
|
+
read/write the same cell. Namespace keys for anything non-global.
|
|
34
|
+
- **Reset with `clearNuxtState(key?)`** — clears one key or all keyed state (e.g.
|
|
35
|
+
on logout).
|
|
36
|
+
|
|
37
|
+
```typescript
|
|
38
|
+
// composables/useFilters.ts — app-wide filter state, survives navigation
|
|
39
|
+
export const useFilters = () => useState("filters", () => ({ search: "", page: 1 }));
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
## `useState` vs the alternatives
|
|
43
|
+
|
|
44
|
+
| Need | Reach for |
|
|
45
|
+
|---|---|
|
|
46
|
+
| Shared reactive state across components, SSR-safe | `useState` |
|
|
47
|
+
| Per-request state that also persists in the browser across reloads | `useCookie` (small, ≤4 KB) — see [ssr-client.md](./ssr-client.md) |
|
|
48
|
+
| Client-only persistence (no SSR) | VueUse `useLocalStorage` — see [ssr-client.md](./ssr-client.md) |
|
|
49
|
+
| Cached server data | `useFetch`/`useAsyncData` (already keyed state) — see [fetch-patterns.md](./fetch-patterns.md) |
|
|
50
|
+
|
|
51
|
+
`useState` is plain shared state; it does NOT fetch or cache. For server data,
|
|
52
|
+
reach for `useAsyncData`/`useFetch` (which are themselves keyed payload state) and
|
|
53
|
+
invalidate with `refreshNuxtData` rather than mirroring fetched data into a
|
|
54
|
+
separate `useState`.
|
|
55
|
+
|
|
56
|
+
## Run one-time init with `callOnce`
|
|
57
|
+
|
|
58
|
+
To run a side effect **exactly once** across SSR + hydration (seed a store, fire a
|
|
59
|
+
one-time analytics/init call), use `callOnce` — not `onMounted` + a flag, which
|
|
60
|
+
re-fires on every client mount:
|
|
61
|
+
|
|
62
|
+
```typescript
|
|
63
|
+
await callOnce("init-analytics", () => initAnalytics());
|
|
64
|
+
// mode: "navigation" (3.15+) re-runs once per client-side navigation instead of once ever
|
|
65
|
+
```
|
|
66
|
+
|
|
67
|
+
`callOnce` returns nothing — it's for effects. For data, use `useAsyncData` (which
|
|
68
|
+
already de-dupes). The call must be unconditional (don't put it behind an `if`).
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
# Storage (unstorage / `useStorage`)
|
|
2
|
+
|
|
3
|
+
Nitro's `useStorage()` is a unified KV abstraction (powered by unstorage). It's
|
|
4
|
+
the right tool for cross-request ephemeral state — rate-limit counters,
|
|
5
|
+
idempotency keys, short-lived caches, one-off blobs — **without** a Postgres
|
|
6
|
+
table. Auto-imported in `server/`.
|
|
7
|
+
|
|
8
|
+
> This is a KV store, not the app database. Persistent relational data still lives
|
|
9
|
+
> in Kysely/Postgres (`server/utils/db.ts`). Note both are called `useDatabase` in
|
|
10
|
+
> their respective worlds — see [server-runtime.md](./server-runtime.md).
|
|
11
|
+
|
|
12
|
+
## Basic use
|
|
13
|
+
|
|
14
|
+
```typescript
|
|
15
|
+
const store = useStorage("redis"); // a configured mount, or useStorage() for default (memory/fs)
|
|
16
|
+
|
|
17
|
+
await store.setItem("rate:user:42", { count: 1, resetAt });
|
|
18
|
+
const hit = await store.getItem<{ count: number }>("rate:user:42");
|
|
19
|
+
await store.removeItem("rate:user:42");
|
|
20
|
+
const keys = await store.getKeys("rate:"); // prefix scan
|
|
21
|
+
```
|
|
22
|
+
|
|
23
|
+
`setItem`/`getItem` JSON-serialize automatically; use `getItemRaw`/`setItemRaw`
|
|
24
|
+
for binary/strings you don't want parsed.
|
|
25
|
+
|
|
26
|
+
## Configuring mounts
|
|
27
|
+
|
|
28
|
+
```typescript
|
|
29
|
+
// nuxt.config.ts
|
|
30
|
+
export default defineNuxtConfig({
|
|
31
|
+
nitro: {
|
|
32
|
+
storage: {
|
|
33
|
+
redis: { driver: "redis", url: process.env.REDIS_URL },
|
|
34
|
+
},
|
|
35
|
+
devStorage: {
|
|
36
|
+
redis: { driver: "fs", base: "./.data/redis" }, // dev override → local fs
|
|
37
|
+
},
|
|
38
|
+
},
|
|
39
|
+
});
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
## Read bundled server assets
|
|
43
|
+
|
|
44
|
+
Files under `server/assets/` are readable (read-only) via the `assets:server`
|
|
45
|
+
mount — handy for seed data, templates, or fixtures shipped with the build:
|
|
46
|
+
|
|
47
|
+
```typescript
|
|
48
|
+
const seed = await useStorage("assets:server").getItem("seed.json");
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
## Gotchas
|
|
52
|
+
|
|
53
|
+
- **Default mount is memory (dev) — not durable.** For anything that must survive
|
|
54
|
+
a restart or be shared across instances, configure a real driver (redis, fs, a
|
|
55
|
+
cloud KV).
|
|
56
|
+
- **Multi-instance:** an in-memory mount is per-process; counters/locks across
|
|
57
|
+
replicas need a shared driver (redis).
|
|
58
|
+
- **The `cache` mount** is what [caching.md](./caching.md) writes to — point it at
|
|
59
|
+
redis in prod so cached responses are shared.
|
|
@@ -36,6 +36,14 @@ Hand-writing one means you've guessed the PrimeVue part structure and the
|
|
|
36
36
|
`surface-*`/`dark:` conventions; the generator gets both right and stays
|
|
37
37
|
consistent with upstream.
|
|
38
38
|
|
|
39
|
+
**`volt-vue add` is a one-time fetch, not a sync channel.** Volt's docs are
|
|
40
|
+
explicit that vendored components [aren't meant to be updated](https://volt.primevue.org/overview/)
|
|
41
|
+
— once added, the file is yours and editing it is the *expected* path, not a
|
|
42
|
+
fallback. There's no `volt-vue update`. Upstream **behavior/structure** fixes
|
|
43
|
+
arrive by bumping the **`primevue`** version (the unstyled logic is imported from
|
|
44
|
+
it); the **styling** is yours to keep. So "edit the vendored source" carries no
|
|
45
|
+
hidden regeneration penalty — that escape hatch was never there to lose.
|
|
46
|
+
|
|
39
47
|
## Customization — `pt:` pass-through
|
|
40
48
|
|
|
41
49
|
Volt uses PrimeVue's pass-through API. Target a component's internal section with
|
|
@@ -58,6 +66,27 @@ differ in precedence:
|
|
|
58
66
|
So `<VoltInputText pt:root:class="bg-primary" />` works; `<VoltInputText
|
|
59
67
|
class="bg-primary" />` may silently not.
|
|
60
68
|
|
|
69
|
+
Both paths run through **`ptViewMerge` in `src/volt/utils.ts`**
|
|
70
|
+
(`twMerge(globalClass, selfClass)` + Vue `mergeProps`), wired onto every component
|
|
71
|
+
via `:ptOptions="{ mergeProps: ptViewMerge }"`. That local helper — not a global
|
|
72
|
+
config — is the actual override point; it's *why* `pt:` reliably wins. To change
|
|
73
|
+
merge behavior across components, edit `utils.ts`. See [gotchas.md](./gotchas.md).
|
|
74
|
+
|
|
75
|
+
### Restyle component states with `p-*` variants
|
|
76
|
+
|
|
77
|
+
Per-state styling (active, focus, disabled, invalid) is expressed declaratively in
|
|
78
|
+
the class string via `tailwindcss-primeui` variants — `p-selected:`, `p-focus:`,
|
|
79
|
+
`p-disabled:`, `p-editable:`, `p-invalid:` (you'll see them throughout the
|
|
80
|
+
vendored sources). They're plain Tailwind variants, so you can **override a state's
|
|
81
|
+
look through `pt:`** without touching DOM or source:
|
|
82
|
+
|
|
83
|
+
```vue
|
|
84
|
+
<VoltSelect pt:option:class="p-selected:bg-highlight p-focus:bg-surface-100" />
|
|
85
|
+
```
|
|
86
|
+
|
|
87
|
+
Reach for these before editing vendored source — restyling the *active* or
|
|
88
|
+
*disabled* appearance rarely needs a source edit.
|
|
89
|
+
|
|
61
90
|
What `pt:` **can't** do is change DOM — it only restyles sections the component
|
|
62
91
|
already renders. To add an element the component doesn't have (e.g. an animated
|
|
63
92
|
overlay), you have two honest options, because Volt components are **vendored and
|
|
@@ -72,30 +101,33 @@ components are **vendored and editable**, so "the component doesn't do X" has
|
|
|
72
101
|
three answers, not two: restyle via `pt:`, **edit the source in `src/volt/`**, or
|
|
73
102
|
build standalone.
|
|
74
103
|
|
|
75
|
-
Worked example — **segmented toggle** (`SelectButton` vs a custom `SlidingTabs`)
|
|
104
|
+
Worked example — **segmented toggle** (`SelectButton` vs a custom `SlidingTabs`).
|
|
105
|
+
*Illustrative:* `SelectButton` isn't vendored in every project (e.g. not in this
|
|
106
|
+
repo's `src/volt/` by default) — you'd `npx volt-vue add SelectButton` first.
|
|
76
107
|
|
|
77
108
|
- `VoltSelectButton` ships v-model, single/multi-select, label+icon options, and
|
|
78
109
|
proper radiogroup a11y, with a *highlighted-active* look.
|
|
79
110
|
- A custom `SlidingTabs` adds an **animated indicator** (a single shared element
|
|
80
111
|
that measures the active button and slides), responsive label collapse, and
|
|
81
112
|
token-matched styling.
|
|
82
|
-
- The
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
`
|
|
86
|
-
|
|
113
|
+
- The active/focus/disabled *look* **is** `pt:`-reachable (via the `p-selected:`/
|
|
114
|
+
`p-focus:` variants above). What `pt:` can't add is the **shared sliding
|
|
115
|
+
element** — `SelectButton` toggles a per-button background and has no single
|
|
116
|
+
position-measured overlay, and `pt:` changes classes, not DOM. Adding that
|
|
117
|
+
indicator means editing the vendored `SelectButton.vue` (you keep its a11y +
|
|
118
|
+
selection model) or building standalone.
|
|
87
119
|
|
|
88
120
|
So the real decision:
|
|
89
121
|
|
|
90
122
|
- **No animation needed → `SelectButton` as-is** (`pt:` to match your design).
|
|
91
123
|
Free a11y + multi-select; don't reinvent it.
|
|
92
124
|
- **Animated indicator / responsive collapse needed →** either **edit the
|
|
93
|
-
vendored `SelectButton`** (keep its a11y
|
|
94
|
-
|
|
95
|
-
(clean and decoupled, but you owe the a11y
|
|
96
|
-
`aria-pressed` at minimum). Standalone wins when
|
|
97
|
-
than a form field; editing the source wins when
|
|
98
|
-
semantics.
|
|
125
|
+
vendored `SelectButton`** (keep its a11y; you own the file — which is true the
|
|
126
|
+
moment you `volt-vue add` it anyway, so there's no regeneration to forfeit) **or
|
|
127
|
+
build a standalone `SlidingTabs`** (clean and decoupled, but you owe the a11y
|
|
128
|
+
yourself — `role="group"` + `aria-pressed` at minimum). Standalone wins when
|
|
129
|
+
it's a *filter* toggle rather than a form field; editing the source wins when
|
|
130
|
+
you want the full input semantics.
|
|
99
131
|
|
|
100
132
|
## Theming & dark mode
|
|
101
133
|
|
|
@@ -124,3 +156,15 @@ See **[gotchas.md](./gotchas.md)** — the ones that cost real debugging time:
|
|
|
124
156
|
reactive `prefers-color-scheme` palette.
|
|
125
157
|
- A bare `boolean` prop casts to `false` when absent — default it with
|
|
126
158
|
`withDefaults`, never rely on `undefined`.
|
|
159
|
+
- The class merge lives in `ptViewMerge` (`src/volt/utils.ts`) — the lever when a
|
|
160
|
+
`pt:` override won't take.
|
|
161
|
+
- `data-pc-name` / `data-pc-section` to reach internals from CSS; `pc`-prefixed
|
|
162
|
+
section names (`pt:pcBadge:…`) for nested child components.
|
|
163
|
+
- We deliberately **don't** use `@primevue/forms` — zod + manual wiring instead.
|
|
164
|
+
|
|
165
|
+
## Plugin config
|
|
166
|
+
|
|
167
|
+
See **[config.md](./config.md)** for PrimeVue plugin options beyond colors —
|
|
168
|
+
global `pt` defaults, `ptOptions` merge behavior, `zIndex` overlay stacking, and
|
|
169
|
+
`locale`. (Keep `unstyled: true`; styled-mode `definePreset`/`theme.preset` are
|
|
170
|
+
inert — see [theming.md](./theming.md).)
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
# PrimeVue plugin config
|
|
2
|
+
|
|
3
|
+
Options passed where PrimeVue is registered (`app.use(PrimeVue, { … })` / the Nuxt
|
|
4
|
+
module config), beyond colors. Volt runs **`unstyled: true`** — keep that; it's
|
|
5
|
+
what makes the components unstyled (and what makes styled-mode theming inert; see
|
|
6
|
+
[theming.md](./theming.md)).
|
|
7
|
+
|
|
8
|
+
## App-wide section styling: global `pt`
|
|
9
|
+
|
|
10
|
+
Instead of repeating the same `pt:` on every call site, set defaults once with a
|
|
11
|
+
global `pt` object keyed by lowercase component name → sections. A component-level
|
|
12
|
+
`pt:` still overrides the global:
|
|
13
|
+
|
|
14
|
+
```ts
|
|
15
|
+
app.use(PrimeVue, {
|
|
16
|
+
unstyled: true,
|
|
17
|
+
pt: {
|
|
18
|
+
dialog: { header: { class: "border-b border-line" } }, // every dialog's header
|
|
19
|
+
select: { dropdown: { class: "text-fg-muted" } },
|
|
20
|
+
},
|
|
21
|
+
ptOptions: { mergeSections: true, mergeProps: true },
|
|
22
|
+
});
|
|
23
|
+
```
|
|
24
|
+
|
|
25
|
+
`ptOptions` governs how global + local combine: `mergeSections` (default `true`)
|
|
26
|
+
merges section objects; `mergeProps` (default `false`) merges class/listener
|
|
27
|
+
props rather than replacing. Set `mergeProps: true` if you want a call-site
|
|
28
|
+
`pt:` to *add to* the global classes instead of replacing them. (Note the vendored
|
|
29
|
+
components already pass their own `mergeProps: ptViewMerge` per-component — see
|
|
30
|
+
[gotchas.md](./gotchas.md).)
|
|
31
|
+
|
|
32
|
+
## Config knobs worth knowing
|
|
33
|
+
|
|
34
|
+
```ts
|
|
35
|
+
app.use(PrimeVue, {
|
|
36
|
+
unstyled: true,
|
|
37
|
+
zIndex: { modal: 1100, overlay: 1000, menu: 1000, tooltip: 1100 }, // overlay stacking
|
|
38
|
+
locale: { /* aria, filter, date strings — overrides built-in en defaults */ },
|
|
39
|
+
});
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
- **`zIndex`** — the dial when a Volt overlay (dialog, menu, tooltip) renders
|
|
43
|
+
under or over app chrome. Defaults: modal/tooltip `1100`, overlay/menu `1000`.
|
|
44
|
+
- **`locale`** — overrides all built-in aria labels, filter/date/pagination
|
|
45
|
+
strings (i18n, or just rewording an aria label).
|
|
46
|
+
- **`inputVariant: 'outlined' | 'filled'`** — default `outlined`; affects input
|
|
47
|
+
styling defaults.
|
|
48
|
+
- **`ripple`** — irrelevant under unstyled (no ripple CSS), harmless if set.
|
|
49
|
+
|
|
50
|
+
Don't set `theme` / `definePreset` — inert under `unstyled: true`
|
|
51
|
+
([theming.md](./theming.md)).
|
|
@@ -64,3 +64,52 @@ const props = withDefaults(defineProps<{ responsive?: boolean }>(), { responsive
|
|
|
64
64
|
Not Volt-specific, but it bit the `SlidingTabs` responsive collapse, so it lives
|
|
65
65
|
here. Any "defaults to on" boolean prop must use `withDefaults` (or invert it to
|
|
66
66
|
an opt-out flag that naturally defaults false).
|
|
67
|
+
|
|
68
|
+
## The class merge lives in `src/volt/utils.ts` (`ptViewMerge`)
|
|
69
|
+
|
|
70
|
+
When a `pt:` class won't override, or you want to change how a component's own
|
|
71
|
+
classes combine with yours, the lever is **not** a global PrimeVue config — it's a
|
|
72
|
+
local helper. Every vendored component sets `:ptOptions="{ mergeProps: ptViewMerge }"`,
|
|
73
|
+
and `ptViewMerge` (in `src/volt/utils.ts`) does `twMerge(globalClass, selfClass)`
|
|
74
|
+
then Vue `mergeProps`:
|
|
75
|
+
|
|
76
|
+
```ts
|
|
77
|
+
export const ptViewMerge = (globalPTProps = {}, selfPTProps = {}, datasets) => {
|
|
78
|
+
const { class: globalClass, ...globalRest } = globalPTProps;
|
|
79
|
+
const { class: selfClass, ...selfRest } = selfPTProps;
|
|
80
|
+
return mergeProps({ class: twMerge(globalClass, selfClass) }, globalRest, selfRest, datasets);
|
|
81
|
+
};
|
|
82
|
+
```
|
|
83
|
+
|
|
84
|
+
That `twMerge` is why a conflicting `pt:` utility wins (last-writer in the merge)
|
|
85
|
+
and a plain `class` may not. Debugging an override that won't take? Look here, not
|
|
86
|
+
at a config flag.
|
|
87
|
+
|
|
88
|
+
## Reaching internals from CSS: `data-pc-name` / `data-pc-section`
|
|
89
|
+
|
|
90
|
+
When `pt:` class styling isn't enough — you need to style a component's internals
|
|
91
|
+
from a parent `<style>` block or third-party CSS — PrimeVue stamps stable
|
|
92
|
+
`data-pc-name="<component>"` and `data-pc-section="<section>"` attributes on its
|
|
93
|
+
DOM. Target those instead of brittle structural selectors:
|
|
94
|
+
|
|
95
|
+
```css
|
|
96
|
+
[data-pc-name="select"] [data-pc-section="dropdown"] { /* … */ }
|
|
97
|
+
```
|
|
98
|
+
|
|
99
|
+
## Styling a nested child component: the `pc`-prefixed section
|
|
100
|
+
|
|
101
|
+
When one PrimeVue component renders another inside it, the inner one's `pt`
|
|
102
|
+
section name is prefixed **`pc`** (e.g. a Badge embedded in another component is
|
|
103
|
+
`pcBadge`). Address it through the prefix; a flat section name silently fails:
|
|
104
|
+
|
|
105
|
+
```vue
|
|
106
|
+
<VoltSomething pt:pcBadge:root:class="bg-red-500" />
|
|
107
|
+
```
|
|
108
|
+
|
|
109
|
+
## We don't use `@primevue/forms`
|
|
110
|
+
|
|
111
|
+
This stack validates with **zod + manual wiring**, not `@primevue/forms`. The
|
|
112
|
+
package's `zodResolver` looks tempting given how much zod is already around, but
|
|
113
|
+
its ergonomics weren't worth the workarounds. Don't reach for it — drive
|
|
114
|
+
validation from the zod schema directly (`safeParse` in the submit handler, or a
|
|
115
|
+
small `useFormState`-style composable).
|
|
@@ -28,12 +28,13 @@ descending order of weight:
|
|
|
28
28
|
0–950 ramp — subtle fills, two border weights, icon idle vs hover, elevation
|
|
29
29
|
layering. To express all of Volt in tokens you'd expand them until they *are*
|
|
30
30
|
the ramp, at which point you've just renamed `surface-*`.
|
|
31
|
-
2. **
|
|
32
|
-
upstream `surface-*` + `dark:` convention
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
31
|
+
2. **Consistency with newly-added siblings.** `volt-vue add` always scaffolds in
|
|
32
|
+
the upstream `surface-*` + `dark:` convention, so keeping existing components on
|
|
33
|
+
it means every Volt file reads the same way and a freshly-added one drops in
|
|
34
|
+
without restyling. This is a mild *consistency* benefit, **not** a regeneration
|
|
35
|
+
one: there's no `volt-vue update` and no continuous sync — you own each file the
|
|
36
|
+
moment you add it, edits or not. (Upstream behavior fixes ride the `primevue`
|
|
37
|
+
version bump, not a re-add.)
|
|
37
38
|
3. **The convention they ship in.** As generated, `--p-surface-0…950` are fixed
|
|
38
39
|
(never flipped), so a Volt component flips by *picking the dark end* with
|
|
39
40
|
`dark:bg-surface-900` — not by a value that changes. Semantic tokens flip the
|
|
@@ -101,6 +102,19 @@ Different namespaces, no collision: `bg-surface-0` (primeui, numbered) and
|
|
|
101
102
|
`prefers-color-scheme` block — your `--color-*` overrides next to Volt's `--p-*`
|
|
102
103
|
overrides.
|
|
103
104
|
|
|
105
|
+
## Ignore styled-mode theming (`definePreset`, `theme.preset`)
|
|
106
|
+
|
|
107
|
+
Most PrimeVue theming docs describe **styled** mode — `definePreset`, `theme: {
|
|
108
|
+
preset: Aura }`, component design tokens like `button.background`. Volt runs
|
|
109
|
+
PrimeVue with **`unstyled: true`**, which turns all of that **off**: the built-in
|
|
110
|
+
CSS and preset machinery aren't loaded, so a `definePreset`/`theme.preset` config
|
|
111
|
+
does nothing here. If you're following a primevue.org theming tutorial and it
|
|
112
|
+
reaches for `definePreset`, stop — that's the wrong layer. Your knobs are the
|
|
113
|
+
`--p-*` variables in `:root` (Volt's way) plus the Tailwind `@theme` tokens above.
|
|
114
|
+
|
|
115
|
+
(The `--p-*` variable **prefix** still applies — `tailwindcss-primeui` reads it.
|
|
116
|
+
It's the preset/design-token *machinery* that's inert, not the variables.)
|
|
117
|
+
|
|
104
118
|
## The naming trap (this one bites)
|
|
105
119
|
|
|
106
120
|
In Tailwind v4 the utility name is the **full** variable suffix:
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "vue-nuxt",
|
|
3
|
+
"description": "Vue 3 component authoring inside a Nuxt 4 app: auto-import rules, props/emits/withDefaults, v-model/defineModel, reactivity, when watch is a smell, and template idioms",
|
|
4
|
+
"version": "1.0.0",
|
|
5
|
+
"author": {
|
|
6
|
+
"name": "yeedle"
|
|
7
|
+
}
|
|
8
|
+
}
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
---
|
|
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, reactivity, when watch is a code smell, and Vue-shaped template idioms.
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# Vue-in-Nuxt component authoring
|
|
7
|
+
|
|
8
|
+
Patterns for writing Vue 3 `<script setup>` components inside a Nuxt 4 app. This
|
|
9
|
+
is the **frontend authoring** slice — the data layer (`useFetch`/`$fetch`, SSR
|
|
10
|
+
storage, hydration, `definePageMeta`/auth, formatters) lives in the
|
|
11
|
+
`nuxt-nitro-api` skill; Volt/PrimeVue styling and dark mode live in
|
|
12
|
+
`volt-primevue`. This skill cross-links to those rather than restating them.
|
|
13
|
+
|
|
14
|
+
## When to Use This Skill
|
|
15
|
+
|
|
16
|
+
- Authoring or reviewing a `.vue` component in a Nuxt 4 project
|
|
17
|
+
- Deciding how a component is named / auto-imported
|
|
18
|
+
- Typing props & emits, defaulting props, building a generic component
|
|
19
|
+
- Wiring `v-model` on a component
|
|
20
|
+
- Designing a component's content API — props vs slots, named/scoped slots
|
|
21
|
+
- Authoring a composable — argument shape, what to return, cleanup
|
|
22
|
+
- Anything reactivity-shaped: `computed` vs `watch`, prop→state sync, DOM measurement
|
|
23
|
+
- You see `watch` and want to know if it should be something else
|
|
24
|
+
|
|
25
|
+
## Reference Files
|
|
26
|
+
|
|
27
|
+
- [auto-imports.md](./auto-imports.md) — what auto-imports (components with dir-prefix names, composables, utils, Vue/Nuxt APIs) and what does NOT (third-party, types, test files)
|
|
28
|
+
- [component-authoring.md](./component-authoring.md) — type-only `defineProps`/`defineEmits`, `withDefaults`, the Boolean-prop trap, factory defaults, generic components, `defineExpose`, what to extract into a shared component
|
|
29
|
+
- [v-model.md](./v-model.md) — `defineModel` vs the props+emit+computed proxy, named models, paired fields
|
|
30
|
+
- [slots.md](./slots.md) — slots vs props for markup, named/scoped slots, `defineSlots`/`useSlots`, avoiding empty wrappers, forwarding, slot transitions
|
|
31
|
+
- [composables.md](./composables.md) — `MaybeRefOrGetter`/`toValue` argument contract, return refs not `reactive()`, thin pure-core shell, `onScopeDispose`/`effectScope` cleanup
|
|
32
|
+
- [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
|
|
33
|
+
- [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
|
|
34
|
+
- [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
|
|
35
|
+
|
|
36
|
+
## Core Principles
|
|
37
|
+
|
|
38
|
+
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>`).
|
|
39
|
+
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).
|
|
40
|
+
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).
|
|
41
|
+
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).
|
|
42
|
+
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.
|
|
43
|
+
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.
|
|
44
|
+
|
|
45
|
+
## Contributing Back
|
|
46
|
+
|
|
47
|
+
If you hit a Vue-in-Nuxt authoring gotcha this skill doesn't cover (or one it gets
|
|
48
|
+
wrong), upstream it — run `/contribute-skill`.
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
# Nuxt auto-imports
|
|
2
|
+
|
|
3
|
+
Nuxt auto-imports your own UI surface and the Vue/Nuxt APIs. Add an explicit
|
|
4
|
+
`import` only for third-party symbols and TypeScript types. Knowing exactly what
|
|
5
|
+
resolves — and how component tags are named — prevents the silent "renders
|
|
6
|
+
nothing" failures.
|
|
7
|
+
|
|
8
|
+
## Components: the tag carries the directory prefix
|
|
9
|
+
|
|
10
|
+
Files in `app/components` auto-import with **no manual import**. A nested
|
|
11
|
+
component's tag is the PascalCased **directory path prefixed onto** the
|
|
12
|
+
PascalCased filename:
|
|
13
|
+
|
|
14
|
+
| File | Tag |
|
|
15
|
+
|---|---|
|
|
16
|
+
| `app/components/AppHeader.vue` | `<AppHeader>` |
|
|
17
|
+
| `app/components/customers/ProfileCard.vue` | `<CustomersProfileCard>` |
|
|
18
|
+
| `app/components/form/FileUpload.vue` | `<FormFileUpload>` |
|
|
19
|
+
| `app/components/settings/Sidebar.vue` | `<SettingsSidebar>` |
|
|
20
|
+
|
|
21
|
+
- **Name to avoid stutter.** Put `ProfileCard.vue` in `customers/` → `<CustomersProfileCard>`, not `CustomerProfileCard.vue` → `<CustomersCustomerProfileCard>`.
|
|
22
|
+
- **Guessing the bare filename fails silently.** `<FileUpload>` for `form/FileUpload.vue` resolves to nothing — no error, just an unrendered tag. If a component "isn't showing up," check the prefix first.
|
|
23
|
+
- **Want a clean unprefixed tag?** Either explicitly import it
|
|
24
|
+
(`import EstimateHeader from '~/components/estimates/EstimateHeader.vue'`), or
|
|
25
|
+
register a directory with a prefix in `nuxt.config` (`components: [{ path: '../src/volt', prefix: 'Volt' }]` → `<VoltButton>`).
|
|
26
|
+
|
|
27
|
+
## What auto-imports (no `import` line)
|
|
28
|
+
|
|
29
|
+
- **Vue reactivity & lifecycle:** `ref`, `shallowRef`, `reactive`, `computed`, `watch`, `watchEffect`, `onWatcherCleanup`, `toValue`/`toRef`/`toRefs`, `useTemplateRef`, `useId`, `effectScope`/`onScopeDispose`, `onMounted`, `onBeforeUnmount`/`onUnmounted`, `nextTick`, `defineProps`/`defineEmits`/`defineModel`/`withDefaults`/`defineOptions`, `resolveComponent`.
|
|
30
|
+
- **Nuxt helpers:** `useRoute`, `useRouter`, `navigateTo`, `useFetch`, `$fetch`, `useAsyncData`, `useState`, `useCookie`, `useRuntimeConfig`, `useHead`, `definePageMeta`, `useNuxtApp`.
|
|
31
|
+
- **Your `app/composables`** (`useFoo`) and **`app/utils`** (pure helpers, by bare name) — app-wide.
|
|
32
|
+
- **`<NuxtLink>`, `<NuxtPage>`, `<NuxtLayout>`, `<ClientOnly>`, `<Teleport>`** in templates.
|
|
33
|
+
|
|
34
|
+
Reach for `#imports` (`import { useFoo } from '#imports'`) only to disambiguate a
|
|
35
|
+
naming collision.
|
|
36
|
+
|
|
37
|
+
## What does NOT auto-import (import explicitly)
|
|
38
|
+
|
|
39
|
+
- **Third-party composables/components:** `useToast` from `'primevue/usetoast'`, `Column` from `'primevue/column'`, `date-fns` helpers, etc.
|
|
40
|
+
- **TypeScript types** — interfaces, enums, type aliases are never auto-imported. `import type { Foo } from '...'`.
|
|
41
|
+
- **`server/utils`** — auto-imported on the *server* only, never into client code.
|
|
42
|
+
- **Vitest unit tests** — plain `*.test.ts` files do NOT get Nuxt's auto-import context. Import the composable/util under test explicitly: `import { useFormatters } from './useFormatters'`. (Component tests via `@nuxt/test-utils/runtime` + `mountSuspended` DO have the context — see the `nitro-testing` skill.)
|
|
43
|
+
|
|
44
|
+
## Consistency caveat
|
|
45
|
+
|
|
46
|
+
Some repos still write explicit `import { ref } from 'vue'` everywhere. **Match
|
|
47
|
+
the file you're editing** — but the default for new code is to rely on
|
|
48
|
+
auto-import.
|
|
@@ -0,0 +1,159 @@
|
|
|
1
|
+
# Component authoring
|
|
2
|
+
|
|
3
|
+
Type-only macros, props/emits, the Boolean trap, generics, and what to extract.
|
|
4
|
+
|
|
5
|
+
## Props & emits: type-only macros
|
|
6
|
+
|
|
7
|
+
Declare props and emits with the type-only generic form; emit payloads as named
|
|
8
|
+
tuple types:
|
|
9
|
+
|
|
10
|
+
```ts
|
|
11
|
+
const props = defineProps<{ label: string; size?: 'sm' | 'md'; rows: Row[] }>()
|
|
12
|
+
const emit = defineEmits<{
|
|
13
|
+
'update:modelValue': [value: string]
|
|
14
|
+
saved: []
|
|
15
|
+
rowClick: [row: Row]
|
|
16
|
+
}>()
|
|
17
|
+
```
|
|
18
|
+
|
|
19
|
+
Use the runtime/object form only when you need `withDefaults`.
|
|
20
|
+
|
|
21
|
+
### Forward many props / handlers at once
|
|
22
|
+
|
|
23
|
+
Spread a whole object of props with `v-bind="obj"`, or a map of handlers with
|
|
24
|
+
`v-on="handlers"`, instead of listing each:
|
|
25
|
+
|
|
26
|
+
```vue
|
|
27
|
+
<UserCard v-bind="user" v-on="cardHandlers" />
|
|
28
|
+
<!-- equivalent to :name="user.name" :email="user.email" … @edit="…" @delete="…" -->
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
Handy for forwarding `$attrs`/`$props` straight through a thin wrapper component
|
|
32
|
+
(`<Inner v-bind="$attrs" />`). Be deliberate, though — spreading a large object
|
|
33
|
+
binds *every* key, which can pass props the child didn't ask for.
|
|
34
|
+
|
|
35
|
+
When you forward `$attrs` onto a specific inner element, set `inheritAttrs: false`
|
|
36
|
+
via **`defineOptions`** so Vue doesn't *also* dump them on the root — the only
|
|
37
|
+
`<script setup>`-native way to set component options (`name`, `inheritAttrs`):
|
|
38
|
+
|
|
39
|
+
```ts
|
|
40
|
+
defineOptions({ inheritAttrs: false })
|
|
41
|
+
// then: <input v-bind="$attrs" /> — attrs land only where you put them
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
## Defaulting props: reactive destructure (3.5) vs `withDefaults`
|
|
45
|
+
|
|
46
|
+
As of Vue 3.5, you can **destructure** `defineProps` and give defaults with `=`.
|
|
47
|
+
The compiler rewrites each reference back to `props.x`, so reactivity is
|
|
48
|
+
preserved, and — unlike `withDefaults` — **non-primitive defaults need no
|
|
49
|
+
factory**. This is now the idiomatic way to default props:
|
|
50
|
+
|
|
51
|
+
```ts
|
|
52
|
+
const { responsive = true, size = 'md', items = [] } = defineProps<{
|
|
53
|
+
responsive?: boolean; size?: 'sm' | 'md'; items?: Item[]
|
|
54
|
+
}>()
|
|
55
|
+
// items = [] is safe here — no shared-reference leak, no factory needed
|
|
56
|
+
```
|
|
57
|
+
|
|
58
|
+
One gotcha: passing a destructured prop into a `watch` source or a standalone
|
|
59
|
+
function **snapshots** it (you destructured a value, not a ref), losing
|
|
60
|
+
reactivity. Wrap it in a getter:
|
|
61
|
+
|
|
62
|
+
```ts
|
|
63
|
+
watch(() => responsive, onChange) // ✅ watch(responsive, …) ❌ passes a bool, never re-fires
|
|
64
|
+
```
|
|
65
|
+
|
|
66
|
+
`withDefaults(defineProps<…>(), {…})` remains correct and is what you'll see in
|
|
67
|
+
existing code — match the file you're in. The two sections below describe the
|
|
68
|
+
traps `withDefaults` has that destructure defaults sidestep.
|
|
69
|
+
|
|
70
|
+
## The Boolean prop trap (the most-cited authoring gotcha)
|
|
71
|
+
|
|
72
|
+
A bare `boolean` prop is subject to Vue's **Boolean casting**: when the attribute
|
|
73
|
+
is absent it coerces to **`false`**, not `undefined`. So `props.flag ?? true`
|
|
74
|
+
never sees `undefined`, and a "defaults-on" flag silently stays off.
|
|
75
|
+
|
|
76
|
+
```ts
|
|
77
|
+
// ❌ absent → false → feature silently disabled (typechecks fine)
|
|
78
|
+
const props = defineProps<{ responsive?: boolean }>()
|
|
79
|
+
const on = props.responsive !== false // always false when not passed
|
|
80
|
+
|
|
81
|
+
// ✅ default it explicitly
|
|
82
|
+
const props = withDefaults(defineProps<{ responsive?: boolean }>(), { responsive: true })
|
|
83
|
+
```
|
|
84
|
+
|
|
85
|
+
Any boolean that should default **on** MUST be defaulted explicitly — via
|
|
86
|
+
destructure (`const { responsive = true } = defineProps<…>()`) or `withDefaults`
|
|
87
|
+
— or be inverted to an opt-*out* flag that naturally defaults `false`. (The
|
|
88
|
+
Boolean casting still happens; the default just gives the absent case a value.)
|
|
89
|
+
|
|
90
|
+
## Factory defaults for arrays/objects
|
|
91
|
+
|
|
92
|
+
Non-primitive defaults need a **factory** in `withDefaults`, or every instance
|
|
93
|
+
shares one object:
|
|
94
|
+
|
|
95
|
+
```ts
|
|
96
|
+
withDefaults(defineProps<{ items?: Item[]; config?: Cfg }>(), {
|
|
97
|
+
items: () => [], // ✅ factory
|
|
98
|
+
config: () => ({ x: 1 }), // ✅ factory
|
|
99
|
+
})
|
|
100
|
+
// ❌ { items: [] } — one array shared across all instances, leaks state
|
|
101
|
+
```
|
|
102
|
+
|
|
103
|
+
Bare literals are only safe for primitives (`isAdmin: false`, `size: 'md'`). This
|
|
104
|
+
factory requirement is a `withDefaults`-ism — reactive destructure defaults
|
|
105
|
+
(`{ items = [] } = defineProps(…)`) take a plain literal safely.
|
|
106
|
+
|
|
107
|
+
## Generic components
|
|
108
|
+
|
|
109
|
+
Declare the type param in the tag and thread it through props/emits so a
|
|
110
|
+
tabs/select preserves the caller's literal union instead of widening to `string`:
|
|
111
|
+
|
|
112
|
+
```vue
|
|
113
|
+
<script setup lang="ts" generic="T extends string">
|
|
114
|
+
const props = defineProps<{ modelValue: T; options: readonly { value: T; label: string }[] }>()
|
|
115
|
+
const emit = defineEmits<{ 'update:modelValue': [value: T] }>()
|
|
116
|
+
</script>
|
|
117
|
+
```
|
|
118
|
+
|
|
119
|
+
## `defineExpose` — the sanctioned imperative escape hatch
|
|
120
|
+
|
|
121
|
+
Expose child methods for a parent to call (reset a form after save, trigger
|
|
122
|
+
submit from a dialog footer) instead of prop hacks:
|
|
123
|
+
|
|
124
|
+
```ts
|
|
125
|
+
defineExpose({ reset, submit })
|
|
126
|
+
// parent: const child = useTemplateRef('child'); child.value?.reset()
|
|
127
|
+
```
|
|
128
|
+
|
|
129
|
+
## What to put in a shared component vs leave in the caller
|
|
130
|
+
|
|
131
|
+
When extracting, a **composable shares logic-only**; a **component shares
|
|
132
|
+
markup + logic**. Extract only the genuinely universal surface and leave
|
|
133
|
+
page-specific chrome in each caller. E.g. a reusable `<Dropzone>` owns the
|
|
134
|
+
drag/drop box and file handling; the page keeps its own heading, helper banner,
|
|
135
|
+
and "or paste text" affordance. If you find yourself adding props just to toggle
|
|
136
|
+
caller-specific chrome inside the shared component, that chrome belongs in the
|
|
137
|
+
caller.
|
|
138
|
+
|
|
139
|
+
## Type off the server contract, don't hand-write DTOs
|
|
140
|
+
|
|
141
|
+
Derive prop/state types from the data you fetch rather than redeclaring an
|
|
142
|
+
interface that drifts from the API:
|
|
143
|
+
|
|
144
|
+
```ts
|
|
145
|
+
// from a fetch ref:
|
|
146
|
+
type Project = NonNullable<typeof projects.value>[number]
|
|
147
|
+
// or an endpoint-response helper indexing the generated InternalApi (illustrative —
|
|
148
|
+
// the general rule is "derive from the endpoint type, never hand-write the DTO";
|
|
149
|
+
// see nuxt-nitro-api/fetch-patterns.md type-extraction)
|
|
150
|
+
```
|
|
151
|
+
|
|
152
|
+
## Advanced: config-driven generic shells
|
|
153
|
+
|
|
154
|
+
A generic table/form shell can take a typed config array
|
|
155
|
+
(`columns: Column[]`, `fields: FormField[]`) whose entries optionally carry a
|
|
156
|
+
`template?: Component` rendered via `<component :is="col.template" v-bind="...">`,
|
|
157
|
+
plus `defineExpose`d imperative methods. Powerful, but it's an advanced pattern —
|
|
158
|
+
reach for it only when several call sites genuinely share the shell, not as a
|
|
159
|
+
default.
|