@gallopsystems/agent-skills 1.2.0 → 1.5.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 +58 -0
- package/commands/contribute-skill.md +65 -0
- package/package.json +2 -1
- package/plugins/copier-template/skills/copier-template/SKILL.md +6 -0
- package/plugins/doctl/skills/doctl/SKILL.md +6 -0
- package/plugins/git-github/skills/git-github/SKILL.md +6 -0
- package/plugins/kysely-postgres/skills/kysely-postgres/SKILL.md +6 -0
- package/plugins/linear/skills/linear/SKILL.md +6 -0
- package/plugins/nitro-testing/skills/nitro-testing/SKILL.md +6 -0
- package/plugins/nuxt-nitro-api/skills/nuxt-nitro-api/SKILL.md +7 -0
- package/plugins/nuxt-nitro-api/skills/nuxt-nitro-api/composables-utils.md +5 -0
- package/plugins/nuxt-nitro-api/skills/nuxt-nitro-api/formatters.md +139 -0
- package/plugins/volt-primevue/.claude-plugin/plugin.json +8 -0
- package/plugins/volt-primevue/skills/volt-primevue/SKILL.md +126 -0
- package/plugins/volt-primevue/skills/volt-primevue/gotchas.md +66 -0
- package/plugins/volt-primevue/skills/volt-primevue/theming.md +123 -0
package/README.md
CHANGED
|
@@ -15,6 +15,10 @@ Then install the skills you want:
|
|
|
15
15
|
/plugin install nuxt-nitro-api@gallop-systems-agent-skills
|
|
16
16
|
/plugin install nitro-testing@gallop-systems-agent-skills
|
|
17
17
|
/plugin install linear@gallop-systems-agent-skills
|
|
18
|
+
/plugin install doctl@gallop-systems-agent-skills
|
|
19
|
+
/plugin install git-github@gallop-systems-agent-skills
|
|
20
|
+
/plugin install copier-template@gallop-systems-agent-skills
|
|
21
|
+
/plugin install volt-primevue@gallop-systems-agent-skills
|
|
18
22
|
```
|
|
19
23
|
|
|
20
24
|
## Updating
|
|
@@ -111,6 +115,60 @@ Covers:
|
|
|
111
115
|
- Async/automation testing utilities
|
|
112
116
|
- CI/CD setup with GitHub Actions and PostgreSQL
|
|
113
117
|
|
|
118
|
+
### linear
|
|
119
|
+
|
|
120
|
+
Create, triage, and manage Linear issues following team conventions, with a GraphQL CLI for operations the Linear MCP server doesn't expose.
|
|
121
|
+
|
|
122
|
+
Covers:
|
|
123
|
+
- Issue creation and triage conventions
|
|
124
|
+
- Tech-stack labels
|
|
125
|
+
- A `linear.mjs` CLI for GraphQL operations
|
|
126
|
+
|
|
127
|
+
### doctl
|
|
128
|
+
|
|
129
|
+
Manage DigitalOcean resources with the doctl CLI.
|
|
130
|
+
|
|
131
|
+
Covers:
|
|
132
|
+
- Auth contexts (per-command `--context` over stateful switching)
|
|
133
|
+
- Resolving the current git repo to its DO context + app
|
|
134
|
+
- App Platform: deployments, bounded polling, logs and their retention quirks
|
|
135
|
+
- App specs: env var/secret round-trips, validation, creating apps
|
|
136
|
+
- `--format` / `-o json` gotchas
|
|
137
|
+
- Managed databases, Spaces keys, droplets, DNS
|
|
138
|
+
|
|
139
|
+
### git-github
|
|
140
|
+
|
|
141
|
+
Git and GitHub (gh CLI) workflows for agents.
|
|
142
|
+
|
|
143
|
+
Covers:
|
|
144
|
+
- Ground rules and the branch → commit → PR loop
|
|
145
|
+
- Reading PR and CI state (`gh pr view --json`, `gh pr checks`)
|
|
146
|
+
- Debugging failed GitHub Actions runs (the full playbook)
|
|
147
|
+
- Repair ladders: rejected pushes, rebase-after-squash-merge, shallow clones, worktrees
|
|
148
|
+
- `gh api` recipes: PR comments, no-checkout file reads, repo settings, PAT gotchas
|
|
149
|
+
- Releases: tags, npm Trusted Publishing, release-please
|
|
150
|
+
- External review loop with the codex CLI
|
|
151
|
+
|
|
152
|
+
### copier-template
|
|
153
|
+
|
|
154
|
+
Maintain a Copier project template and propagate updates to generated repos.
|
|
155
|
+
|
|
156
|
+
Covers:
|
|
157
|
+
- Template anatomy: copier.yml, conditional files, tasks, jinja escaping in CI workflows
|
|
158
|
+
- Testing template changes (`--vcs-ref HEAD`, generate-and-validate)
|
|
159
|
+
- Releasing versions (tag + GitHub Release) and the update-checker workflow pattern
|
|
160
|
+
- Applying `copier update` in descendants: conflict triage, `.rej` files, validation
|
|
161
|
+
|
|
162
|
+
## Contributing a Lesson Back
|
|
163
|
+
|
|
164
|
+
Every skill ends with a **Contributing Back** section: when Claude works through
|
|
165
|
+
something the skill didn't cover, it offers to contribute the lesson upstream. The
|
|
166
|
+
`/contribute-skill` command (shipped in this package and symlinked into
|
|
167
|
+
`.claude/commands/` on install) automates the flow: distill the generic lesson,
|
|
168
|
+
privacy-sweep it, clone or fork this repo, and open a PR against the right skill
|
|
169
|
+
file. PRs from forks are welcome — content must be generic (placeholders only, no
|
|
170
|
+
project-specific names, IDs, or domains).
|
|
171
|
+
|
|
114
172
|
## Adding New Skills
|
|
115
173
|
|
|
116
174
|
1. Create a new plugin directory: `plugins/my-skill/`
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
---
|
|
2
|
+
description: Contribute a lesson learned this session back to the gallop-systems/agent-skills repo as a PR
|
|
3
|
+
argument-hint: [which skill and/or what lesson]
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# Contribute a Lesson Back to the Skills Repo
|
|
7
|
+
|
|
8
|
+
You are turning something learned in this session into a PR against
|
|
9
|
+
https://github.com/gallop-systems/agent-skills, the public repo behind the
|
|
10
|
+
installed skills.
|
|
11
|
+
|
|
12
|
+
## 1. Identify the lesson
|
|
13
|
+
|
|
14
|
+
From this session (or from `$ARGUMENTS` if given), pin down:
|
|
15
|
+
|
|
16
|
+
- **The lesson**: usually an error→fix sequence, a behavior that contradicted the
|
|
17
|
+
skill, or a workflow knot the skill didn't cover. It must be something you
|
|
18
|
+
*verified in this session* — not a guess.
|
|
19
|
+
- **The target**: which skill (`doctl`, `git-github`, `copier-template`,
|
|
20
|
+
`kysely-postgres`, `nuxt-nitro-api`, `nitro-testing`, `linear`), and within it,
|
|
21
|
+
whether it belongs in `SKILL.md` or one of its reference `.md` files. Read the
|
|
22
|
+
target file first and match its structure and tone.
|
|
23
|
+
|
|
24
|
+
If the lesson is ambiguous or you can't verify it, stop and clarify with the user.
|
|
25
|
+
|
|
26
|
+
## 2. Genericize — the repo is public
|
|
27
|
+
|
|
28
|
+
Rewrite the lesson with placeholders only: `<app-id>`, `<owner>/<repo>`, `<branch>`,
|
|
29
|
+
`<domain>`. **No project names, client names, UUIDs, IPs, domains, tokens, or file
|
|
30
|
+
paths from the user's codebase.** Keep it tight: the generic rule, a minimal example
|
|
31
|
+
command, and the failure it prevents — in that order.
|
|
32
|
+
|
|
33
|
+
## 3. Clone, edit, verify
|
|
34
|
+
|
|
35
|
+
Work in a temp directory, never in the user's project:
|
|
36
|
+
|
|
37
|
+
```bash
|
|
38
|
+
dir=$(mktemp -d)
|
|
39
|
+
if [ "$(gh api repos/gallop-systems/agent-skills --jq .permissions.push)" = "true" ]; then
|
|
40
|
+
gh repo clone gallop-systems/agent-skills "$dir"
|
|
41
|
+
else
|
|
42
|
+
gh repo fork gallop-systems/agent-skills --clone "$dir" # outside contributors
|
|
43
|
+
fi
|
|
44
|
+
cd "$dir" && git checkout -b feat/<skill>-<short-slug>
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
Edit the target file under `plugins/<skill>/skills/<skill>/`. Then privacy-sweep
|
|
48
|
+
your diff before committing — grep the changed files for anything resembling the
|
|
49
|
+
user's project (project name, org, hostnames, IDs). If anything hits, fix it.
|
|
50
|
+
|
|
51
|
+
## 4. Commit and open the PR
|
|
52
|
+
|
|
53
|
+
The repo enforces Conventional Commit PR titles (release-please derives versions
|
|
54
|
+
from them). Use `feat(<skill>): <summary>` for new coverage, `fix(<skill>): <summary>`
|
|
55
|
+
for corrections to existing content.
|
|
56
|
+
|
|
57
|
+
```bash
|
|
58
|
+
git add -A && git commit -m "feat(<skill>): document <lesson summary>"
|
|
59
|
+
git push -u origin <branch>
|
|
60
|
+
gh pr create --repo gallop-systems/agent-skills \
|
|
61
|
+
--title "feat(<skill>): <summary>" \
|
|
62
|
+
--body "<what the lesson is, how it was hit and verified (genericized), why it belongs in this skill>"
|
|
63
|
+
```
|
|
64
|
+
|
|
65
|
+
Show the user the PR URL, then clean up: `cd - && rm -rf "$dir"`.
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@gallopsystems/agent-skills",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.5.0",
|
|
4
4
|
"description": "Gallop Systems Claude Code skills, symlinked into .claude/skills on install.",
|
|
5
5
|
"license": "UNLICENSED",
|
|
6
6
|
"repository": {
|
|
@@ -9,6 +9,7 @@
|
|
|
9
9
|
},
|
|
10
10
|
"files": [
|
|
11
11
|
"plugins",
|
|
12
|
+
"commands",
|
|
12
13
|
"scripts/link-skills.mjs",
|
|
13
14
|
"README.md"
|
|
14
15
|
],
|
|
@@ -66,3 +66,9 @@ If newer, it pushes a **static branch name** (e.g. `chore/template-update`) with
|
|
|
66
66
|
|
|
67
67
|
- **Template anatomy & testing changes**: [template-authoring.md](template-authoring.md)
|
|
68
68
|
- **Applying an update in a descendant** (the conflict-resolution procedure): [applying-updates.md](applying-updates.md)
|
|
69
|
+
|
|
70
|
+
## Contributing Back
|
|
71
|
+
|
|
72
|
+
This skill grows by capturing what it missed. If you just worked through something in this domain that this skill did not cover — an error you had to figure out, a behavior that contradicts what is documented above, a workflow knot — ask the user: **"Want me to contribute this back to the copier-template skill?"**
|
|
73
|
+
|
|
74
|
+
If yes, run `/contribute-skill`. If that command is not available, do the equivalent inline: distill the generic lesson (placeholders only — no project names, IDs, domains, or secrets), then branch or fork [gallop-systems/agent-skills](https://github.com/gallop-systems/agent-skills) and open a PR editing this skill.
|
|
@@ -117,3 +117,9 @@ doctl apps logs <app-id> --type run --follow # live tail (interactive
|
|
|
117
117
|
|
|
118
118
|
- **App specs — env vars, secrets, creating apps**: see [spec-management.md](spec-management.md)
|
|
119
119
|
- **Databases, Spaces, droplets, DNS**: see [other-services.md](other-services.md)
|
|
120
|
+
|
|
121
|
+
## Contributing Back
|
|
122
|
+
|
|
123
|
+
This skill grows by capturing what it missed. If you just worked through something in this domain that this skill did not cover — an error you had to figure out, a behavior that contradicts what is documented above, a workflow knot — ask the user: **"Want me to contribute this back to the doctl skill?"**
|
|
124
|
+
|
|
125
|
+
If yes, run `/contribute-skill`. If that command is not available, do the equivalent inline: distill the generic lesson (placeholders only — no project names, IDs, domains, or secrets), then branch or fork [gallop-systems/agent-skills](https://github.com/gallop-systems/agent-skills) and open a PR editing this skill.
|
|
@@ -83,3 +83,9 @@ Do not bypass failing hooks with `--no-verify` unless the user says to.
|
|
|
83
83
|
- **gh api recipes** — PR comments, reading files without checkout, repo settings, PAT gotchas: [gh-api-recipes.md](gh-api-recipes.md)
|
|
84
84
|
- **Releases & publishing** — tags, gh release, npm Trusted Publishing, release-please: [releases.md](releases.md)
|
|
85
85
|
- **External review loop** — using the codex CLI as an adversarial pre-merge reviewer: [external-review.md](external-review.md)
|
|
86
|
+
|
|
87
|
+
## Contributing Back
|
|
88
|
+
|
|
89
|
+
This skill grows by capturing what it missed. If you just worked through something in this domain that this skill did not cover — an error you had to figure out, a behavior that contradicts what is documented above, a workflow knot — ask the user: **"Want me to contribute this back to the git-github skill?"**
|
|
90
|
+
|
|
91
|
+
If yes, run `/contribute-skill`. If that command is not available, do the equivalent inline: distill the generic lesson (placeholders only — no project names, IDs, domains, or secrets), then branch or fork [gallop-systems/agent-skills](https://github.com/gallop-systems/agent-skills) and open a PR editing this skill.
|
|
@@ -1099,3 +1099,9 @@ const result = await db
|
|
|
1099
1099
|
- The asserted type must structurally match the actual type (full type safety preserved)
|
|
1100
1100
|
- Apply to several intermediate `with` clauses in large queries
|
|
1101
1101
|
- TypeScript cannot automatically simplify these types - explicit assertion is required
|
|
1102
|
+
|
|
1103
|
+
## Contributing Back
|
|
1104
|
+
|
|
1105
|
+
This skill grows by capturing what it missed. If you just worked through something in this domain that this skill did not cover — an error you had to figure out, a behavior that contradicts what is documented above, a workflow knot — ask the user: **"Want me to contribute this back to the kysely-postgres skill?"**
|
|
1106
|
+
|
|
1107
|
+
If yes, run `/contribute-skill`. If that command is not available, do the equivalent inline: distill the generic lesson (placeholders only — no project names, IDs, domains, or secrets), then branch or fork [gallop-systems/agent-skills](https://github.com/gallop-systems/agent-skills) and open a PR editing this skill.
|
|
@@ -1038,3 +1038,9 @@ node linear.mjs rebalance
|
|
|
1038
1038
|
4. **Don't overcommit cycles.** Leave ~20% buffer for bugs, client requests, and interruptions.
|
|
1039
1039
|
5. **Projects identify the client.** No need for client prefixes in issue titles or client labels — the project name (e.g., `[GBX] Portal`) already provides that context.
|
|
1040
1040
|
6. **Triage first.** New client requests go to Backlog, not straight into the sprint — unless truly urgent.
|
|
1041
|
+
|
|
1042
|
+
## Contributing Back
|
|
1043
|
+
|
|
1044
|
+
This skill grows by capturing what it missed. If you just worked through something in this domain that this skill did not cover — an error you had to figure out, a behavior that contradicts what is documented above, a workflow knot — ask the user: **"Want me to contribute this back to the linear skill?"**
|
|
1045
|
+
|
|
1046
|
+
If yes, run `/contribute-skill`. If that command is not available, do the equivalent inline: distill the generic lesson (placeholders only — no project names, IDs, domains, or secrets), then branch or fork [gallop-systems/agent-skills](https://github.com/gallop-systems/agent-skills) and open a PR editing this skill.
|
|
@@ -495,3 +495,9 @@ it("updates after user interaction", async () => {
|
|
|
495
495
|
5. **Await everything** - `mountSuspended`, `trigger()`, `nextTick()` are all async.
|
|
496
496
|
|
|
497
497
|
6. **Separate test configs** - Use `VITEST_ENV` to run frontend and backend tests with different environments.
|
|
498
|
+
|
|
499
|
+
## Contributing Back
|
|
500
|
+
|
|
501
|
+
This skill grows by capturing what it missed. If you just worked through something in this domain that this skill did not cover — an error you had to figure out, a behavior that contradicts what is documented above, a workflow knot — ask the user: **"Want me to contribute this back to the nitro-testing skill?"**
|
|
502
|
+
|
|
503
|
+
If yes, run `/contribute-skill`. If that command is not available, do the equivalent inline: distill the generic lesson (placeholders only — no project names, IDs, domains, or secrets), then branch or fork [gallop-systems/agent-skills](https://github.com/gallop-systems/agent-skills) and open a PR editing this skill.
|
|
@@ -25,6 +25,7 @@ For detailed patterns, see these topic-focused reference files:
|
|
|
25
25
|
- [auth-patterns.md](./auth-patterns.md) - nuxt-auth-utils, OAuth, WebAuthn, middleware
|
|
26
26
|
- [page-structure.md](./page-structure.md) - Keep pages thin, components do the work
|
|
27
27
|
- [composables-utils.md](./composables-utils.md) - When to use composables vs utils
|
|
28
|
+
- [formatters.md](./formatters.md) - Centralize currency/date/number formatters in useFormatters, never inline
|
|
28
29
|
- [ssr-client.md](./ssr-client.md) - SSR + localStorage, hydration, VueUse
|
|
29
30
|
- [deep-linking.md](./deep-linking.md) - URL params sync with filters and useFetch
|
|
30
31
|
- [nitro-tasks.md](./nitro-tasks.md) - Background jobs, scheduled tasks, job queues
|
|
@@ -258,3 +259,9 @@ const activeProjects = computed(() =>
|
|
|
258
259
|
```
|
|
259
260
|
|
|
260
261
|
This ensures your frontend types stay in sync with your API - if the endpoint return type changes, TypeScript will catch mismatches.
|
|
262
|
+
|
|
263
|
+
## Contributing Back
|
|
264
|
+
|
|
265
|
+
This skill grows by capturing what it missed. If you just worked through something in this domain that this skill did not cover — an error you had to figure out, a behavior that contradicts what is documented above, a workflow knot — ask the user: **"Want me to contribute this back to the nuxt-nitro-api skill?"**
|
|
266
|
+
|
|
267
|
+
If yes, run `/contribute-skill`. If that command is not available, do the equivalent inline: distill the generic lesson (placeholders only — no project names, IDs, domains, or secrets), then branch or fork [gallop-systems/agent-skills](https://github.com/gallop-systems/agent-skills) and open a PR editing this skill.
|
|
@@ -79,6 +79,11 @@ export const usePermissions = () => {
|
|
|
79
79
|
- Data transformations, formatting, parsing
|
|
80
80
|
- NO `use` prefix
|
|
81
81
|
|
|
82
|
+
> **Formatters belong in one shared place.** The examples below show util *placement*,
|
|
83
|
+
> not where to call formatters from. Never define a currency/date/number formatter inline
|
|
84
|
+
> at the call site — centralize them in `useFormatters` or a shared util, and prefer
|
|
85
|
+
> VueUse / date-fns over hand-rolling. See [formatters.md](./formatters.md).
|
|
86
|
+
|
|
82
87
|
```typescript
|
|
83
88
|
// utils/formatting.ts
|
|
84
89
|
export const formatDate = (date: string) => {
|
|
@@ -0,0 +1,139 @@
|
|
|
1
|
+
# Formatters (Currency, Dates, Numbers)
|
|
2
|
+
|
|
3
|
+
## Rule: Never define a formatter inline
|
|
4
|
+
|
|
5
|
+
Whenever you need to format a value — currency, dates, times, numbers, percentages,
|
|
6
|
+
file sizes, relative time, etc. — **do not define the formatter inline at the call site.**
|
|
7
|
+
|
|
8
|
+
Before writing any new formatting logic:
|
|
9
|
+
|
|
10
|
+
1. **Check if a shared formatter already exists.** Look for a `useFormatters`
|
|
11
|
+
composable (`/composables/useFormatters.ts`) or a formatting util
|
|
12
|
+
(`/utils/formatters.ts`, `/shared/utils/format.ts`). If a formatter for the value
|
|
13
|
+
you need is already there, **use it.**
|
|
14
|
+
2. **If none exists, create one** in the appropriate shared location, then use it.
|
|
15
|
+
Do not scatter a one-off `Intl.NumberFormat(...)` or `new Date(...).toLocaleString(...)`
|
|
16
|
+
at the call site.
|
|
17
|
+
|
|
18
|
+
This keeps formatting consistent across the app (one source of truth for locale,
|
|
19
|
+
currency, date style) and makes a future change — switching locale, adding a currency,
|
|
20
|
+
tweaking date format — a single edit instead of a hunt-and-replace.
|
|
21
|
+
|
|
22
|
+
```typescript
|
|
23
|
+
// WRONG - inline formatter at the call site
|
|
24
|
+
<template>
|
|
25
|
+
<span>{{ new Intl.NumberFormat("en-US", { style: "currency", currency: "USD" }).format(total) }}</span>
|
|
26
|
+
<span>{{ new Date(order.createdAt).toLocaleDateString("en-US", { dateStyle: "medium" }) }}</span>
|
|
27
|
+
</template>
|
|
28
|
+
|
|
29
|
+
// RIGHT - use the shared formatter
|
|
30
|
+
<script setup lang="ts">
|
|
31
|
+
const { formatCurrency, formatDate } = useFormatters();
|
|
32
|
+
</script>
|
|
33
|
+
<template>
|
|
34
|
+
<span>{{ formatCurrency(total) }}</span>
|
|
35
|
+
<span>{{ formatDate(order.createdAt) }}</span>
|
|
36
|
+
</template>
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
## Reach for existing libraries before hand-rolling
|
|
40
|
+
|
|
41
|
+
Two libraries are almost always already available — prefer them over writing formatting
|
|
42
|
+
or date logic by hand:
|
|
43
|
+
|
|
44
|
+
- **VueUse** ships a broad set of formatting/reactive helpers out of the box (auto-imported
|
|
45
|
+
in Nuxt). Before building your own, check for one of these:
|
|
46
|
+
- `useDateFormat(date, "YYYY-MM-DD HH:mm")` — reactive date formatting
|
|
47
|
+
- `useTimeAgo(date)` — reactive "3 minutes ago" relative time
|
|
48
|
+
- `useNow()` / `useTimestamp()` — reactive current time to drive the above
|
|
49
|
+
- `formatTimeAgo()` — the non-reactive function form
|
|
50
|
+
Wrap these in `useFormatters` when you want a single app-wide configuration point,
|
|
51
|
+
rather than calling them ad hoc at each site.
|
|
52
|
+
|
|
53
|
+
- **date-fns is the preferred way to work with dates.** Do **not** parse, compare, add,
|
|
54
|
+
or diff dates by hand (no manual `string.split("-")`, no `new Date(a) - new Date(b)`
|
|
55
|
+
arithmetic, no hand-rolled "is same day"). Use `date-fns`:
|
|
56
|
+
|
|
57
|
+
```typescript
|
|
58
|
+
import { format, parseISO, formatDistanceToNow, differenceInDays, isSameDay } from "date-fns";
|
|
59
|
+
|
|
60
|
+
format(parseISO(order.createdAt), "MMM d, yyyy"); // "Jun 12, 2026"
|
|
61
|
+
formatDistanceToNow(parseISO(order.createdAt)); // "about 2 hours"
|
|
62
|
+
differenceInDays(parseISO(end), parseISO(start)); // 5
|
|
63
|
+
```
|
|
64
|
+
|
|
65
|
+
```typescript
|
|
66
|
+
// WRONG - parsing/diffing dates by hand
|
|
67
|
+
const [y, m, d] = order.createdAt.split("T")[0].split("-");
|
|
68
|
+
const daysLeft = Math.floor((new Date(end) - new Date(start)) / 86400000);
|
|
69
|
+
```
|
|
70
|
+
|
|
71
|
+
Still route date-fns calls through `useFormatters` (or a shared util) rather than
|
|
72
|
+
importing and calling them inline everywhere — same single-source-of-truth reason.
|
|
73
|
+
|
|
74
|
+
## Where to put the formatters
|
|
75
|
+
|
|
76
|
+
Pick the location by what the formatter needs (see [composables-utils.md](./composables-utils.md)):
|
|
77
|
+
|
|
78
|
+
- **Needs Nuxt/Vue context** (e.g. reads locale/currency from `useRuntimeConfig()`,
|
|
79
|
+
`useI18n()`, or user preferences) → **composable** `useFormatters` in
|
|
80
|
+
`/composables/useFormatters.ts`.
|
|
81
|
+
- **Pure, client-only** → **util** in `/utils/formatters.ts`.
|
|
82
|
+
- **Used on both client and server** (e.g. an invoice rendered in SSR *and* in a
|
|
83
|
+
server API response) → **shared util** in `/shared/utils/format.ts`.
|
|
84
|
+
|
|
85
|
+
When in doubt and the formatters are pure, prefer `useFormatters` as a composable so
|
|
86
|
+
there is one obvious, discoverable place to look — and so it can later pull locale
|
|
87
|
+
from context without moving every call site.
|
|
88
|
+
|
|
89
|
+
### Composable form (`useFormatters`)
|
|
90
|
+
|
|
91
|
+
```typescript
|
|
92
|
+
// composables/useFormatters.ts
|
|
93
|
+
export const useFormatters = () => {
|
|
94
|
+
const config = useRuntimeConfig();
|
|
95
|
+
const locale = config.public.locale ?? "en-US";
|
|
96
|
+
const currency = config.public.currency ?? "USD";
|
|
97
|
+
|
|
98
|
+
const currencyFmt = new Intl.NumberFormat(locale, { style: "currency", currency });
|
|
99
|
+
const dateFmt = new Intl.DateTimeFormat(locale, { dateStyle: "medium" });
|
|
100
|
+
const dateTimeFmt = new Intl.DateTimeFormat(locale, { dateStyle: "medium", timeStyle: "short" });
|
|
101
|
+
const numberFmt = new Intl.NumberFormat(locale);
|
|
102
|
+
const percentFmt = new Intl.NumberFormat(locale, { style: "percent", maximumFractionDigits: 1 });
|
|
103
|
+
|
|
104
|
+
return {
|
|
105
|
+
formatCurrency: (amount: number) => currencyFmt.format(amount),
|
|
106
|
+
formatDate: (date: string | Date) => dateFmt.format(new Date(date)),
|
|
107
|
+
formatDateTime: (date: string | Date) => dateTimeFmt.format(new Date(date)),
|
|
108
|
+
formatNumber: (n: number) => numberFmt.format(n),
|
|
109
|
+
formatPercent: (n: number) => percentFmt.format(n),
|
|
110
|
+
};
|
|
111
|
+
};
|
|
112
|
+
```
|
|
113
|
+
|
|
114
|
+
> **Reuse the `Intl.*` instances.** Construct each formatter once (as above), not on
|
|
115
|
+
> every call. `Intl.NumberFormat`/`Intl.DateTimeFormat` construction is comparatively
|
|
116
|
+
> expensive, so building a new one inside a render or a loop is wasteful.
|
|
117
|
+
|
|
118
|
+
### Pure util / shared form
|
|
119
|
+
|
|
120
|
+
If no Nuxt context is needed, the same functions live as plain exports:
|
|
121
|
+
|
|
122
|
+
```typescript
|
|
123
|
+
// shared/utils/format.ts (or utils/formatters.ts for client-only)
|
|
124
|
+
const currencyFmt = new Intl.NumberFormat("en-US", { style: "currency", currency: "USD" });
|
|
125
|
+
const dateFmt = new Intl.DateTimeFormat("en-US", { dateStyle: "medium" });
|
|
126
|
+
|
|
127
|
+
export const formatCurrency = (amount: number) => currencyFmt.format(amount);
|
|
128
|
+
export const formatDate = (date: string | Date) => dateFmt.format(new Date(date));
|
|
129
|
+
```
|
|
130
|
+
|
|
131
|
+
## Checklist before adding a formatter
|
|
132
|
+
|
|
133
|
+
- [ ] Searched for an existing `useFormatters` / `formatters` / `format` util?
|
|
134
|
+
- [ ] Reusing it if a matching formatter exists?
|
|
135
|
+
- [ ] Checked for a VueUse helper (`useDateFormat`, `useTimeAgo`, …) before hand-rolling?
|
|
136
|
+
- [ ] Using `date-fns` for any date parsing/formatting/math — not hand-parsing strings?
|
|
137
|
+
- [ ] If creating, placed it in the right shared location (composable vs util vs shared)?
|
|
138
|
+
- [ ] Constructed the `Intl.*` instance once, not per call?
|
|
139
|
+
- [ ] Replaced the inline formatting at the call site with the shared function?
|
|
@@ -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,126 @@
|
|
|
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
|
+
## Customization — `pt:` pass-through
|
|
40
|
+
|
|
41
|
+
Volt uses PrimeVue's pass-through API. Target a component's internal section with
|
|
42
|
+
`pt:<section>:class`:
|
|
43
|
+
|
|
44
|
+
```vue
|
|
45
|
+
<VoltButton pt:root:class="bg-zinc-900 hover:bg-zinc-800" />
|
|
46
|
+
<VoltCard pt:root:class="rounded-2xl" pt:body:class="p-6" />
|
|
47
|
+
```
|
|
48
|
+
|
|
49
|
+
**Use `pt:`, not a plain `class`.** Volt merges classes with
|
|
50
|
+
[tailwind-merge](https://volt.primevue.org/overview/#twmerge), and the two paths
|
|
51
|
+
differ in precedence:
|
|
52
|
+
|
|
53
|
+
- `pt:{section}:class` is **merged with the component's defaults** and reliably
|
|
54
|
+
overrides them — `pt:root:class="bg-primary"` wins.
|
|
55
|
+
- A plain `class="bg-primary"` has **lower precedence and may not apply** — its
|
|
56
|
+
conflicting utilities can lose to the component's own classes.
|
|
57
|
+
|
|
58
|
+
So `<VoltInputText pt:root:class="bg-primary" />` works; `<VoltInputText
|
|
59
|
+
class="bg-primary" />` may silently not.
|
|
60
|
+
|
|
61
|
+
What `pt:` **can't** do is change DOM — it only restyles sections the component
|
|
62
|
+
already renders. To add an element the component doesn't have (e.g. an animated
|
|
63
|
+
overlay), you have two honest options, because Volt components are **vendored and
|
|
64
|
+
yours to edit** (see [Choosing a component](#choosing-a-component-volt-vs-custom)):
|
|
65
|
+
edit the component's source in `src/volt/`, or build a standalone component.
|
|
66
|
+
|
|
67
|
+
## Choosing a component: Volt vs custom
|
|
68
|
+
|
|
69
|
+
Volt gives you selection semantics + accessibility for free. The question is only
|
|
70
|
+
whether you need DOM/behavior the component doesn't render — and remember Volt
|
|
71
|
+
components are **vendored and editable**, so "the component doesn't do X" has
|
|
72
|
+
three answers, not two: restyle via `pt:`, **edit the source in `src/volt/`**, or
|
|
73
|
+
build standalone.
|
|
74
|
+
|
|
75
|
+
Worked example — **segmented toggle** (`SelectButton` vs a custom `SlidingTabs`):
|
|
76
|
+
|
|
77
|
+
- `VoltSelectButton` ships v-model, single/multi-select, label+icon options, and
|
|
78
|
+
proper radiogroup a11y, with a *highlighted-active* look.
|
|
79
|
+
- A custom `SlidingTabs` adds an **animated indicator** (a single shared element
|
|
80
|
+
that measures the active button and slides), responsive label collapse, and
|
|
81
|
+
token-matched styling.
|
|
82
|
+
- The slide **can't** come from `pt:` — `SelectButton` toggles a background class
|
|
83
|
+
per button and has no shared, position-measured overlay, and `pt:` changes
|
|
84
|
+
classes, not DOM. But that doesn't force a rewrite: since the source lives in
|
|
85
|
+
`src/volt/SelectButton.vue`, adding the indicator **there** is a legitimate
|
|
86
|
+
option (you keep its a11y + selection model).
|
|
87
|
+
|
|
88
|
+
So the real decision:
|
|
89
|
+
|
|
90
|
+
- **No animation needed → `SelectButton` as-is** (`pt:` to match your design).
|
|
91
|
+
Free a11y + multi-select; don't reinvent it.
|
|
92
|
+
- **Animated indicator / responsive collapse needed →** either **edit the
|
|
93
|
+
vendored `SelectButton`** (keep its a11y, accept that you now own that file and
|
|
94
|
+
lose easy `volt-vue add` regeneration) **or build a standalone `SlidingTabs`**
|
|
95
|
+
(clean and decoupled, but you owe the a11y yourself — `role="group"` +
|
|
96
|
+
`aria-pressed` at minimum). Standalone wins when it's a *filter* toggle rather
|
|
97
|
+
than a form field; editing the source wins when you want the full input
|
|
98
|
+
semantics.
|
|
99
|
+
|
|
100
|
+
## Theming & dark mode
|
|
101
|
+
|
|
102
|
+
This is the part people get wrong. Read **[theming.md](./theming.md)** — the
|
|
103
|
+
two-layer color model (`surface-*` for Volt, semantic tokens for your markup),
|
|
104
|
+
why they can't be unified, and the `prefers-color-scheme` mechanics.
|
|
105
|
+
|
|
106
|
+
The one-paragraph version: dark mode follows the OS via
|
|
107
|
+
`@media (prefers-color-scheme: dark)`. **Your app markup** uses semantic `@theme`
|
|
108
|
+
tokens (`bg-surface`, `text-fg`, `border-line`) written **once** — the
|
|
109
|
+
`--color-*` var flips in the dark media block, so there's no `dark:` half to
|
|
110
|
+
forget. **Volt internals** stay on the `surface-*` scale with explicit `dark:`
|
|
111
|
+
pairs — they need the full 0–950 ramp, and leaving them as `volt-vue add`
|
|
112
|
+
generated them keeps regeneration easy. You *could* restyle a vendored component
|
|
113
|
+
to tokens (it's your code), but the small token set can't express the whole ramp,
|
|
114
|
+
so don't — tokens for your opinionated markup, `surface-*` for the component
|
|
115
|
+
library.
|
|
116
|
+
|
|
117
|
+
## Gotchas
|
|
118
|
+
|
|
119
|
+
See **[gotchas.md](./gotchas.md)** — the ones that cost real debugging time:
|
|
120
|
+
|
|
121
|
+
- `@reference "main.css"` (not `"tailwindcss"`) for `@apply` of custom tokens in
|
|
122
|
+
SFC `<style>` blocks.
|
|
123
|
+
- JS-driven colors (ApexCharts, canvas) can't read CSS tokens — pick them from a
|
|
124
|
+
reactive `prefers-color-scheme` palette.
|
|
125
|
+
- A bare `boolean` prop casts to `false` when absent — default it with
|
|
126
|
+
`withDefaults`, never rely on `undefined`.
|
|
@@ -0,0 +1,66 @@
|
|
|
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).
|
|
@@ -0,0 +1,123 @@
|
|
|
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. **Regeneration convenience.** `volt-vue add` scaffolds a component in the
|
|
32
|
+
upstream `surface-*` + `dark:` convention. Restyle it and you own that file —
|
|
33
|
+
re-adding or pulling a newer version later means redoing your edits. Leaving
|
|
34
|
+
Volt as-generated keeps that escape hatch cheap. (This is a convenience cost,
|
|
35
|
+
not "you diverge from upstream forever" — there's no continuous sync; you add
|
|
36
|
+
once and own it either way.)
|
|
37
|
+
3. **The convention they ship in.** As generated, `--p-surface-0…950` are fixed
|
|
38
|
+
(never flipped), so a Volt component flips by *picking the dark end* with
|
|
39
|
+
`dark:bg-surface-900` — not by a value that changes. Semantic tokens flip the
|
|
40
|
+
variable's value instead, so one class covers both schemes. You could rewrite
|
|
41
|
+
a component to the token style, but then you're back to reasons 1 and 2.
|
|
42
|
+
|
|
43
|
+
## The token set
|
|
44
|
+
|
|
45
|
+
Defined in `app/assets/css/main.css`: light values in a top-level `@theme`
|
|
46
|
+
block, dark values overriding the same `--color-*` vars inside the
|
|
47
|
+
`prefers-color-scheme: dark` media block.
|
|
48
|
+
|
|
49
|
+
```css
|
|
50
|
+
@theme {
|
|
51
|
+
--color-canvas: #fafafa; /* app background */
|
|
52
|
+
--color-surface: #ffffff; /* cards, panels, dialogs */
|
|
53
|
+
--color-surface-muted: #fafafa; /* subtle fills, row hover */
|
|
54
|
+
--color-line: #e4e4e7; /* borders */
|
|
55
|
+
--color-line-soft: #f4f4f5; /* soft dividers */
|
|
56
|
+
--color-fg: #18181b; /* primary text */
|
|
57
|
+
--color-fg-muted: #71717a; /* secondary text */
|
|
58
|
+
--color-fg-subtle: #a1a1aa; /* tertiary text */
|
|
59
|
+
--color-accent: #18181b; /* inverted/primary fills (buttons) */
|
|
60
|
+
--color-on-accent: #ffffff; /* text/icon on an accent fill */
|
|
61
|
+
--color-fill: #e4e4e7; /* neutral solid fills: avatars, tracks, dots */
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
@media (prefers-color-scheme: dark) {
|
|
65
|
+
:root {
|
|
66
|
+
--color-canvas: #09090b;
|
|
67
|
+
--color-surface: #18181b;
|
|
68
|
+
--color-surface-muted: #27272a;
|
|
69
|
+
--color-line: #27272a;
|
|
70
|
+
--color-line-soft: #27272a;
|
|
71
|
+
--color-fg: #f4f4f5;
|
|
72
|
+
--color-fg-muted: #b4b4bc; /* lifted off a strict mirror-invert for legibility */
|
|
73
|
+
--color-fg-subtle: #8c8c95; /* ~5.5:1 on near-black */
|
|
74
|
+
--color-accent: #f4f4f5; /* inverts so filled buttons read on dark */
|
|
75
|
+
--color-on-accent: #18181b;
|
|
76
|
+
--color-fill: #3f3f46;
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
```
|
|
80
|
+
|
|
81
|
+
The dark `fg-muted` / `fg-subtle` are **lifted** off a strict mirror-invert —
|
|
82
|
+
the perfectly-inverted values are too dark to read on near-black.
|
|
83
|
+
|
|
84
|
+
### Where each layer lives (and why it's not fighting Volt)
|
|
85
|
+
|
|
86
|
+
[Volt's Nuxt setup](https://volt.primevue.org/nuxt/#css-variables) prescribes the
|
|
87
|
+
`--p-*` palette + semantic tokens in **`:root`**, with dark mode via
|
|
88
|
+
`@media (prefers-color-scheme: dark)`. Keep that exactly as-is — don't move
|
|
89
|
+
`--p-*` into `@theme`; the `tailwindcss-primeui` plugin already turns them into
|
|
90
|
+
`surface-*` / `primary-*` utilities.
|
|
91
|
+
|
|
92
|
+
Your semantic tokens go in **`@theme`** instead, because that's the Tailwind v4
|
|
93
|
+
mechanism that generates the utilities (`--color-canvas` → `bg-canvas`). Defining
|
|
94
|
+
them only in `:root` would set the variable but produce **no class**. So:
|
|
95
|
+
|
|
96
|
+
- `--p-*` → `:root` (Volt's way; plugin generates `surface-*`)
|
|
97
|
+
- `--color-*` → `@theme` (Tailwind's way; generates `bg-surface`, `text-fg`, …)
|
|
98
|
+
|
|
99
|
+
Different namespaces, no collision: `bg-surface-0` (primeui, numbered) and
|
|
100
|
+
`bg-surface` (your token, bare) coexist. Dark values for **both** sit in the same
|
|
101
|
+
`prefers-color-scheme` block — your `--color-*` overrides next to Volt's `--p-*`
|
|
102
|
+
overrides.
|
|
103
|
+
|
|
104
|
+
## The naming trap (this one bites)
|
|
105
|
+
|
|
106
|
+
In Tailwind v4 the utility name is the **full** variable suffix:
|
|
107
|
+
|
|
108
|
+
- `--color-fg-subtle` → `text-fg-subtle` ✅
|
|
109
|
+
- `text-subtle` ❌ — generates **nothing**, element falls back to near-black
|
|
110
|
+
|
|
111
|
+
A wrong/shortened token name fails silently (no class generated), so a
|
|
112
|
+
suspicious "secondary text is black in dark mode" is almost always a misnamed
|
|
113
|
+
token, not a wrong value. **Compile the CSS and grep the generated utilities** —
|
|
114
|
+
don't eyeball the browser.
|
|
115
|
+
|
|
116
|
+
## Adding a new token
|
|
117
|
+
|
|
118
|
+
1. Add `--color-foo: <light>;` to the `@theme` block.
|
|
119
|
+
2. Add `--color-foo: <dark>;` inside the dark media block.
|
|
120
|
+
3. Use `bg-foo` / `text-foo` / `border-foo` — done, both schemes covered.
|
|
121
|
+
|
|
122
|
+
No `dark:` variant, ever, for app markup. If you're typing `dark:` in a page or
|
|
123
|
+
app component, you're reaching for the wrong layer.
|