@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.
- package/README.md +3 -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/.claude-plugin/plugin.json +8 -0
- package/plugins/volt-primevue/skills/volt-primevue/SKILL.md +170 -0
- package/plugins/volt-primevue/skills/volt-primevue/config.md +51 -0
- package/plugins/volt-primevue/skills/volt-primevue/gotchas.md +115 -0
- package/plugins/volt-primevue/skills/volt-primevue/theming.md +137 -0
- 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.
|
|
@@ -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
|
+
}
|