@gallopsystems/agent-skills 1.4.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 (32) hide show
  1. package/README.md +3 -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/.claude-plugin/plugin.json +8 -0
  19. package/plugins/volt-primevue/skills/volt-primevue/SKILL.md +170 -0
  20. package/plugins/volt-primevue/skills/volt-primevue/config.md +51 -0
  21. package/plugins/volt-primevue/skills/volt-primevue/gotchas.md +115 -0
  22. package/plugins/volt-primevue/skills/volt-primevue/theming.md +137 -0
  23. package/plugins/vue-nuxt/.claude-plugin/plugin.json +8 -0
  24. package/plugins/vue-nuxt/skills/vue-nuxt/SKILL.md +48 -0
  25. package/plugins/vue-nuxt/skills/vue-nuxt/auto-imports.md +48 -0
  26. package/plugins/vue-nuxt/skills/vue-nuxt/component-authoring.md +159 -0
  27. package/plugins/vue-nuxt/skills/vue-nuxt/composables.md +95 -0
  28. package/plugins/vue-nuxt/skills/vue-nuxt/reactivity.md +133 -0
  29. package/plugins/vue-nuxt/skills/vue-nuxt/slots.md +139 -0
  30. package/plugins/vue-nuxt/skills/vue-nuxt/template-idioms.md +142 -0
  31. package/plugins/vue-nuxt/skills/vue-nuxt/v-model.md +106 -0
  32. 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.
@@ -0,0 +1,8 @@
1
+ {
2
+ "name": "volt-primevue",
3
+ "description": "Volt (unstyled PrimeVue + Tailwind) UI: adding components, pt: pass-through styling, the surface-* vs semantic-token color model, and prefers-color-scheme dark mode",
4
+ "version": "1.0.0",
5
+ "author": {
6
+ "name": "yeedle"
7
+ }
8
+ }
@@ -0,0 +1,170 @@
1
+ ---
2
+ name: volt-primevue
3
+ description: Build UIs with Volt (unstyled PrimeVue + Tailwind). Covers adding components, pt: pass-through customization, choosing components, and the two-layer color model for prefers-color-scheme dark mode.
4
+ ---
5
+
6
+ # Volt + PrimeVue UI
7
+
8
+ Volt components are **PrimeVue unstyled components styled with Tailwind**. They
9
+ ship as source you vendor into `src/volt/` and register with a `Volt` prefix, so
10
+ you own the markup but track upstream styling conventions.
11
+
12
+ ## When to Use This Skill
13
+
14
+ - Building UI in a Nuxt + Tailwind v4 + PrimeVue/Volt project
15
+ - Adding or customizing a Volt component
16
+ - Anything color/theme/dark-mode related in this stack
17
+ - Deciding between a Volt component and a hand-rolled one
18
+
19
+ ## What Volt is
20
+
21
+ - PrimeVue **unstyled** components + Tailwind classes, vendored under `src/volt/`.
22
+ - Registered with a `Volt` prefix via `nuxt.config.ts` (`<VoltButton>`, `<VoltCard>`, …).
23
+ - `app/assets/css/main.css` has `@source "../../../src/volt";` so Tailwind scans
24
+ the vendored sources for class names. If a Volt class isn't generating, that
25
+ `@source` line is the first thing to check.
26
+
27
+ ## Adding components
28
+
29
+ Use the CLI — **never hand-create a Volt component**:
30
+
31
+ ```bash
32
+ npx volt-vue add MultiSelect # adds src/volt/MultiSelect.vue
33
+ ```
34
+
35
+ Hand-writing one means you've guessed the PrimeVue part structure and the
36
+ `surface-*`/`dark:` conventions; the generator gets both right and stays
37
+ consistent with upstream.
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
+
47
+ ## Customization — `pt:` pass-through
48
+
49
+ Volt uses PrimeVue's pass-through API. Target a component's internal section with
50
+ `pt:<section>:class`:
51
+
52
+ ```vue
53
+ <VoltButton pt:root:class="bg-zinc-900 hover:bg-zinc-800" />
54
+ <VoltCard pt:root:class="rounded-2xl" pt:body:class="p-6" />
55
+ ```
56
+
57
+ **Use `pt:`, not a plain `class`.** Volt merges classes with
58
+ [tailwind-merge](https://volt.primevue.org/overview/#twmerge), and the two paths
59
+ differ in precedence:
60
+
61
+ - `pt:{section}:class` is **merged with the component's defaults** and reliably
62
+ overrides them — `pt:root:class="bg-primary"` wins.
63
+ - A plain `class="bg-primary"` has **lower precedence and may not apply** — its
64
+ conflicting utilities can lose to the component's own classes.
65
+
66
+ So `<VoltInputText pt:root:class="bg-primary" />` works; `<VoltInputText
67
+ class="bg-primary" />` may silently not.
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
+
90
+ What `pt:` **can't** do is change DOM — it only restyles sections the component
91
+ already renders. To add an element the component doesn't have (e.g. an animated
92
+ overlay), you have two honest options, because Volt components are **vendored and
93
+ yours to edit** (see [Choosing a component](#choosing-a-component-volt-vs-custom)):
94
+ edit the component's source in `src/volt/`, or build a standalone component.
95
+
96
+ ## Choosing a component: Volt vs custom
97
+
98
+ Volt gives you selection semantics + accessibility for free. The question is only
99
+ whether you need DOM/behavior the component doesn't render — and remember Volt
100
+ components are **vendored and editable**, so "the component doesn't do X" has
101
+ three answers, not two: restyle via `pt:`, **edit the source in `src/volt/`**, or
102
+ build standalone.
103
+
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.
107
+
108
+ - `VoltSelectButton` ships v-model, single/multi-select, label+icon options, and
109
+ proper radiogroup a11y, with a *highlighted-active* look.
110
+ - A custom `SlidingTabs` adds an **animated indicator** (a single shared element
111
+ that measures the active button and slides), responsive label collapse, and
112
+ token-matched styling.
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.
119
+
120
+ So the real decision:
121
+
122
+ - **No animation needed → `SelectButton` as-is** (`pt:` to match your design).
123
+ Free a11y + multi-select; don't reinvent it.
124
+ - **Animated indicator / responsive collapse needed →** either **edit the
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.
131
+
132
+ ## Theming & dark mode
133
+
134
+ This is the part people get wrong. Read **[theming.md](./theming.md)** — the
135
+ two-layer color model (`surface-*` for Volt, semantic tokens for your markup),
136
+ why they can't be unified, and the `prefers-color-scheme` mechanics.
137
+
138
+ The one-paragraph version: dark mode follows the OS via
139
+ `@media (prefers-color-scheme: dark)`. **Your app markup** uses semantic `@theme`
140
+ tokens (`bg-surface`, `text-fg`, `border-line`) written **once** — the
141
+ `--color-*` var flips in the dark media block, so there's no `dark:` half to
142
+ forget. **Volt internals** stay on the `surface-*` scale with explicit `dark:`
143
+ pairs — they need the full 0–950 ramp, and leaving them as `volt-vue add`
144
+ generated them keeps regeneration easy. You *could* restyle a vendored component
145
+ to tokens (it's your code), but the small token set can't express the whole ramp,
146
+ so don't — tokens for your opinionated markup, `surface-*` for the component
147
+ library.
148
+
149
+ ## Gotchas
150
+
151
+ See **[gotchas.md](./gotchas.md)** — the ones that cost real debugging time:
152
+
153
+ - `@reference "main.css"` (not `"tailwindcss"`) for `@apply` of custom tokens in
154
+ SFC `<style>` blocks.
155
+ - JS-driven colors (ApexCharts, canvas) can't read CSS tokens — pick them from a
156
+ reactive `prefers-color-scheme` palette.
157
+ - A bare `boolean` prop casts to `false` when absent — default it with
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)).
@@ -0,0 +1,115 @@
1
+ # Gotchas
2
+
3
+ The ones that cost real debugging time in this stack.
4
+
5
+ ## `@apply` of custom tokens in `<style>` needs `@reference "main.css"`
6
+
7
+ Tailwind v4 SFC `<style>` blocks need a `@reference` to resolve `@apply`. The
8
+ trap: `@reference "tailwindcss"` only loads the **default** theme — built-in
9
+ colors (`zinc`, etc.) work, but your custom `@theme` tokens (`text-fg`,
10
+ `bg-surface`) fail with *"Cannot apply unknown utility class."*
11
+
12
+ ```vue
13
+ <style scoped>
14
+ /* ❌ @reference "tailwindcss"; → @apply text-fg fails */
15
+ @reference "../assets/css/main.css"; /* ✅ exposes your tokens */
16
+ .prose :where(h2) { @apply text-fg; }
17
+ </style>
18
+ ```
19
+
20
+ `@reference` is relative to the SFC. Note `yarn build` validates `@apply` in
21
+ `<style>`; a bare `@tailwindcss/cli` compile does **not**, so the CLI can pass
22
+ while the real build fails — include `build` in your check.
23
+
24
+ ## JS-driven colors can't read CSS tokens — use a reactive palette
25
+
26
+ Anything that sets colors as JS values (ApexCharts, canvas, SVG attributes
27
+ written from script) can't use `bg-surface`/`var(--color-fg)` — the value is
28
+ baked at render. Pick concrete colors from a reactive `prefers-color-scheme`
29
+ match and recompute on change:
30
+
31
+ ```ts
32
+ const isDark = ref(false);
33
+ let mq: MediaQueryList | null = null;
34
+ const sync = () => (isDark.value = mq?.matches ?? false);
35
+ onMounted(() => {
36
+ mq = window.matchMedia("(prefers-color-scheme: dark)");
37
+ sync();
38
+ mq.addEventListener("change", sync);
39
+ });
40
+ onBeforeUnmount(() => mq?.removeEventListener("change", sync));
41
+
42
+ const palette = computed(() =>
43
+ isDark.value ? { fg: "#f4f4f5", line: "#f4f4f5", grid: "#27272a" }
44
+ : { fg: "#18181b", line: "#18181b", grid: "#f4f4f5" });
45
+ ```
46
+
47
+ Mirror the dark values from `main.css`. A chart `colors: ["#18181b"]` (near-black)
48
+ is invisible on dark — invert the line to a light value via the palette.
49
+
50
+ ## A bare `boolean` prop casts to `false` when absent — `withDefaults` it
51
+
52
+ Vue's Boolean-prop casting: a prop typed `boolean` with **no default** coerces to
53
+ `false` when the parent doesn't pass it — *not* `undefined`. So a "default-on"
54
+ flag written as `responsive?: boolean` + `props.responsive !== false` is always
55
+ `false` unless explicitly passed, and the feature silently never fires.
56
+
57
+ ```ts
58
+ // ❌ absent → false → feature off, no error
59
+ const props = defineProps<{ responsive?: boolean }>();
60
+ // ✅ absent → true
61
+ const props = withDefaults(defineProps<{ responsive?: boolean }>(), { responsive: true });
62
+ ```
63
+
64
+ Not Volt-specific, but it bit the `SlidingTabs` responsive collapse, so it lives
65
+ here. Any "defaults to on" boolean prop must use `withDefaults` (or invert it to
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).
@@ -0,0 +1,137 @@
1
+ # Theming & dark mode
2
+
3
+ Dark mode follows the OS: `@media (prefers-color-scheme: dark)`. No toggle, no
4
+ `dark` class on `<html>`. Tailwind v4's `dark:` variant also keys on
5
+ `prefers-color-scheme` by default, so everything reacts to the same signal.
6
+
7
+ There are **two color systems** in play, and the whole skill is knowing which to
8
+ use where.
9
+
10
+ ## The two layers
11
+
12
+ | | App markup (you own) | Volt internals (vendored) |
13
+ |---|---|---|
14
+ | **System** | semantic `@theme` tokens | `surface-*` scale + `dark:` pairs |
15
+ | **Example** | `bg-surface text-fg` | `bg-surface-0 dark:bg-surface-900` |
16
+ | **How it flips** | the `--color-*` var changes value in the dark media block | each shade is fixed; the component picks the dark end with `dark:` |
17
+ | **You write** | once | both halves |
18
+
19
+ ### Why not use semantic tokens everywhere (incl. Volt)?
20
+
21
+ You *can* — Volt components are vendored and editable, so nothing stops you from
22
+ restyling one to `bg-surface text-fg`. The reasons not to are practical, in
23
+ descending order of weight:
24
+
25
+ 1. **Vocabulary mismatch (the real one).** Semantic tokens are a small,
26
+ opinionated set (`surface`, `surface-muted`, `line`, `fg`, `fg-muted`, …) for
27
+ "card / text / border" decisions. A component library reaches all over the
28
+ 0–950 ramp — subtle fills, two border weights, icon idle vs hover, elevation
29
+ layering. To express all of Volt in tokens you'd expand them until they *are*
30
+ the ramp, at which point you've just renamed `surface-*`.
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.)
38
+ 3. **The convention they ship in.** As generated, `--p-surface-0…950` are fixed
39
+ (never flipped), so a Volt component flips by *picking the dark end* with
40
+ `dark:bg-surface-900` — not by a value that changes. Semantic tokens flip the
41
+ variable's value instead, so one class covers both schemes. You could rewrite
42
+ a component to the token style, but then you're back to reasons 1 and 2.
43
+
44
+ ## The token set
45
+
46
+ Defined in `app/assets/css/main.css`: light values in a top-level `@theme`
47
+ block, dark values overriding the same `--color-*` vars inside the
48
+ `prefers-color-scheme: dark` media block.
49
+
50
+ ```css
51
+ @theme {
52
+ --color-canvas: #fafafa; /* app background */
53
+ --color-surface: #ffffff; /* cards, panels, dialogs */
54
+ --color-surface-muted: #fafafa; /* subtle fills, row hover */
55
+ --color-line: #e4e4e7; /* borders */
56
+ --color-line-soft: #f4f4f5; /* soft dividers */
57
+ --color-fg: #18181b; /* primary text */
58
+ --color-fg-muted: #71717a; /* secondary text */
59
+ --color-fg-subtle: #a1a1aa; /* tertiary text */
60
+ --color-accent: #18181b; /* inverted/primary fills (buttons) */
61
+ --color-on-accent: #ffffff; /* text/icon on an accent fill */
62
+ --color-fill: #e4e4e7; /* neutral solid fills: avatars, tracks, dots */
63
+ }
64
+
65
+ @media (prefers-color-scheme: dark) {
66
+ :root {
67
+ --color-canvas: #09090b;
68
+ --color-surface: #18181b;
69
+ --color-surface-muted: #27272a;
70
+ --color-line: #27272a;
71
+ --color-line-soft: #27272a;
72
+ --color-fg: #f4f4f5;
73
+ --color-fg-muted: #b4b4bc; /* lifted off a strict mirror-invert for legibility */
74
+ --color-fg-subtle: #8c8c95; /* ~5.5:1 on near-black */
75
+ --color-accent: #f4f4f5; /* inverts so filled buttons read on dark */
76
+ --color-on-accent: #18181b;
77
+ --color-fill: #3f3f46;
78
+ }
79
+ }
80
+ ```
81
+
82
+ The dark `fg-muted` / `fg-subtle` are **lifted** off a strict mirror-invert —
83
+ the perfectly-inverted values are too dark to read on near-black.
84
+
85
+ ### Where each layer lives (and why it's not fighting Volt)
86
+
87
+ [Volt's Nuxt setup](https://volt.primevue.org/nuxt/#css-variables) prescribes the
88
+ `--p-*` palette + semantic tokens in **`:root`**, with dark mode via
89
+ `@media (prefers-color-scheme: dark)`. Keep that exactly as-is — don't move
90
+ `--p-*` into `@theme`; the `tailwindcss-primeui` plugin already turns them into
91
+ `surface-*` / `primary-*` utilities.
92
+
93
+ Your semantic tokens go in **`@theme`** instead, because that's the Tailwind v4
94
+ mechanism that generates the utilities (`--color-canvas` → `bg-canvas`). Defining
95
+ them only in `:root` would set the variable but produce **no class**. So:
96
+
97
+ - `--p-*` → `:root` (Volt's way; plugin generates `surface-*`)
98
+ - `--color-*` → `@theme` (Tailwind's way; generates `bg-surface`, `text-fg`, …)
99
+
100
+ Different namespaces, no collision: `bg-surface-0` (primeui, numbered) and
101
+ `bg-surface` (your token, bare) coexist. Dark values for **both** sit in the same
102
+ `prefers-color-scheme` block — your `--color-*` overrides next to Volt's `--p-*`
103
+ overrides.
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
+
118
+ ## The naming trap (this one bites)
119
+
120
+ In Tailwind v4 the utility name is the **full** variable suffix:
121
+
122
+ - `--color-fg-subtle` → `text-fg-subtle` ✅
123
+ - `text-subtle` ❌ — generates **nothing**, element falls back to near-black
124
+
125
+ A wrong/shortened token name fails silently (no class generated), so a
126
+ suspicious "secondary text is black in dark mode" is almost always a misnamed
127
+ token, not a wrong value. **Compile the CSS and grep the generated utilities** —
128
+ don't eyeball the browser.
129
+
130
+ ## Adding a new token
131
+
132
+ 1. Add `--color-foo: <light>;` to the `@theme` block.
133
+ 2. Add `--color-foo: <dark>;` inside the dark media block.
134
+ 3. Use `bg-foo` / `text-foo` / `border-foo` — done, both schemes covered.
135
+
136
+ No `dark:` variant, ever, for app markup. If you're typing `dark:` in a page or
137
+ app component, you're reaching for the wrong layer.
@@ -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
+ }