',
+ )
+ expect(rewritten).toContain('[&>*]:mt-2')
+ expect(rewritten).toContain('data-[state=open]:block')
+ expect(rewritten).not.toContain('className:[&>*]')
+ expect(rewritten).not.toContain('className:data-[state=open]')
+ })
})
describe('Basic transformations', () => {
diff --git a/src/tests/init-setup.test.ts b/src/tests/init-setup.test.ts
index 75b3393..17ee3c4 100644
--- a/src/tests/init-setup.test.ts
+++ b/src/tests/init-setup.test.ts
@@ -299,19 +299,19 @@ describe('mergeTailwindClassAttributes', () => {
it('merges vue patterns', () => {
const out = mergeTailwindClassAttributes(['class'], 'vue')
expect(out).toContain('class')
- expect(out).toContain('class:[\\w:/@-]*')
+ expect(out).toContain('class:[\\w:/@\\[\\]\\-=&*>.]*')
})
it('adds className for react', () => {
const out = mergeTailwindClassAttributes([], 'react')
expect(out).toContain('className')
- expect(out).toContain('className:[\\w:/@-]*')
+ expect(out).toContain('className:[\\w:/@\\[\\]\\-=&*>.]*')
})
it('uses vue-style patterns for svelte', () => {
const out = mergeTailwindClassAttributes(['class'], 'svelte')
expect(out).toContain('class')
- expect(out).toContain('class:[\\w:/@-]*')
+ expect(out).toContain('class:[\\w:/@\\[\\]\\-=&*>.]*')
expect(out).not.toContain('className')
})
})
diff --git a/tasks/lessons.md b/tasks/lessons.md
index 1f3afcb..260af40 100644
--- a/tasks/lessons.md
+++ b/tasks/lessons.md
@@ -75,7 +75,11 @@
- Do not rewrite UseClassy smoke demos into a polished fictional product UI (Harbor-style inbox, design-system cards, etc.) unless the user has approved a mock after seeing it.
- Coverage pages can stay labeled and a bit clinical; that is easier to scan than a realistic layout that hides the cases. Prefer smaller visual cleanup (copy, titles, spacing) over a full scene rewrite.
-## JSX conditional class rewrites (2026-07-21)
+## React `@` / `/` / arbitrary modifiers (2026-08-25)
+
+- UseClassy rewrites `className:@md`, `className:group-hover/item`, `className:[&>*]`, and `className:data-[state=open]` before JSX/HTML parse — same pipeline as `className:sm:hover`.
+- Parse modifier names with bracket depth so `=` inside `[…]` is not treated as the attribute separator. Do not invent substitute characters or a separate `mods()` helper.
+- TypeScript / some linters may still flag the source the same way they already flag chained modifiers.
- When rewriting string literals inside `className:modifier={…}`, never blindly prefix every quoted string.
- Comparison operands (`===` / `!==` / `==` / `!=`) and string method receivers (`'x'.includes`) must stay untouched.
diff --git a/templates/useclassy-authoring.cursor-rule.mdc b/templates/useclassy-authoring.cursor-rule.mdc
index a33d8c4..d7a16ea 100644
--- a/templates/useclassy-authoring.cursor-rule.mdc
+++ b/templates/useclassy-authoring.cursor-rule.mdc
@@ -34,6 +34,6 @@ When this project uses `vite-plugin-useclassy`, write new static Tailwind varian
- React: prefer double-quoted static strings; `className:mod={cond ? 'a' : 'b'}` is also valid when literals should be prefixed.
- Leave Vue `:class`, Svelte native `class:name={cond}`, and unrelated dynamic base expressions unchanged.
- **Svelte**: only transform quoted UseClassy modifiers (`class:hover="…"`). Do not rewrite native `class:name={cond}` or `class:name`.
-- Keep arbitrary variants such as `[&>*]:mt-2` and `data-[state=open]:block` in the base string. Named groups (`group-hover/item`) and `@md` container queries work as Vue/HTML modifier names; in React JSX, keep named groups on `className` because `/` is not a valid attribute-name character.
+- Keep named groups (`group-hover/item`), `@md` container queries, and arbitrary variants (`[&>*]`, `data-[state=open]`) as modifier attributes in Vue/HTML and React. UseClassy rewrites them before parse (bracket-aware so `=` inside `[…]` is fine).
- Chained attributes match Tailwind/Uno composition: `class:sm:hover="underline"` generates `sm:hover:underline` only. Converting `sm:hover:underline` to `class:sm:hover="underline"` is behavior-preserving.
- After refactoring, run formatting and relevant tests/build; verify dynamic classes and rendered states are unchanged.
diff --git a/templates/useclassy-setup.cursor-rule.mdc b/templates/useclassy-setup.cursor-rule.mdc
index 40d3889..590b194 100644
--- a/templates/useclassy-setup.cursor-rule.mdc
+++ b/templates/useclassy-setup.cursor-rule.mdc
@@ -9,7 +9,7 @@ When adding or fixing UseClassy in this project:
2. In `vite.config.*`, import `useClassy` from `vite-plugin-useclassy` and add `useClassy({ language: 'vue' | 'react' | 'blade' | 'svelte' })` to `plugins` **before** `@tailwindcss/vite` or other CSS plugins. For Svelte, also place it **before** `@sveltejs/vite-plugin-svelte`.
3. **Tailwind v4** (CSS uses `@import "tailwindcss"`): In that stylesheet, add an `@source` line pointing at the generated manifest. Default manifest path is `.classy/output.classy.html` from the project root; the `@source` path must be **relative to the CSS file**. In JavaScript configs you can compute the line with `getUseClassyTailwindSourceDirective(cssAbsolutePath, projectRoot)` from `vite-plugin-useclassy`.
4. **Tailwind v3**: Add `./.classy/output.classy.html` to `content` in `tailwind.config.*` (or use `getUseClassyTailwindV3ContentEntry()` from the package for the default path).
-5. **VS Code** (Tailwind only): Merge `tailwindCSS.classAttributes` to include `class:[\\w:/@-]*` and, for React, `className:[\\w:/@-]*`. Skip this for UnoCSS — use the UnoCSS extension.
+5. **VS Code** (Tailwind only): Merge `tailwindCSS.classAttributes` to include `class:[\\w:/@\\[\\]\\-=&*>.]*` and, for React, `className:[\\w:/@\\[\\]\\-=&*>.]*`. Skip this for UnoCSS — use the UnoCSS extension.
6. Run dev once so `.classy/output.classy.html` is generated.
Prefer `npx vite-plugin-useclassy init` (after installing the package) instead of hand-editing when possible. Use `--with-skills` to install the UseClassy authoring skill (`.agents/skills`) plus these Cursor rules. Add `--with-claude` if you also use Claude Code.
diff --git a/templates/useclassy-skill/SKILL.md b/templates/useclassy-skill/SKILL.md
index 8cf7bf7..dc27f08 100644
--- a/templates/useclassy-skill/SKILL.md
+++ b/templates/useclassy-skill/SKILL.md
@@ -25,16 +25,16 @@ Use UseClassy to separate Tailwind variants from base utilities:
## Syntax
-| Language | Base | Modifiers |
-|----------|------|-----------|
-| Vue / Blade | `class="…"` | `class:hover="…"`, `class:sm:hover="…"` |
-| React | `className="…"` | `className:hover="…"` (also accepts `class:…`); JSX expressions allowed |
-| Svelte | `class="…"` | Quoted only: `class:hover="…"` |
+| Language | Base | Modifiers |
+| ----------- | --------------- | ----------------------------------------------------------------------- |
+| Vue / Blade | `class="…"` | `class:hover="…"`, `class:sm:hover="…"` |
+| React | `className="…"` | `className:hover="…"`, `className:@md="…"`, `className:group-hover/item="…"`, `className:[&>*]="…"`, `className:data-[state=open]="…"`; JSX expressions allowed |
+| Svelte | `class="…"` | Quoted only: `class:hover="…"` |
-Modifier names may contain letters, numbers, `_`, `-`, `:`, `/` (named groups such as `group-hover/item`), and `@` (container queries such as `@md`). Arbitrary variants (`[&>*]`, `data-[state=open]`) cannot be attribute names — leave those tokens on the base class. In React JSX, `/` in an attribute name is invalid, so named groups must stay on `className`.
+Modifier names may contain letters, numbers, `_`, `-`, `:`, `/` (named groups such as `group-hover/item`), `@` (container queries such as `@md`), and arbitrary variants with `[…]` (`[&>*]`, `data-[state=open]`). UseClassy parses modifier names with bracket depth so `=` inside `[…]` is not the attribute separator. React uses the same modifier attributes as Vue; UseClassy rewrites them before JSX/HTML parse.
-- **Vue / Blade / Svelte / HTML:** modifier values must be double-quoted static class strings.
-- **React:** prefer double-quoted static strings. JSX expressions are also supported when string literals inside the expression should receive the variant prefix, e.g. `className:hover={on ? 'bg-blue-500' : 'bg-gray-200'}`.
+- **Vue / Blade / Svelte / HTML:** modifier values must be quoted static class strings (`"` or `'`).
+- **React:** prefer quoted static strings (`"` or `'`). JSX expressions are also supported when string literals inside the expression should receive the variant prefix, e.g. `className:hover={on ? 'bg-blue-500' : 'bg-gray-200'}`.
## Refactor existing code
@@ -53,7 +53,6 @@ When asked to convert markup to UseClassy:
Convert only static tokens that can be represented safely. Do not rewrite:
- Dynamic expressions, template interpolations, conditional class helpers, Vue `:class`, or Svelte directives — unless you are intentionally using React's `className:mod={…}` expression form with string literals.
-- Arbitrary variant prefixes such as `[&>*]:mt-2` or `data-[state=open]:block`; their characters are not valid in a UseClassy modifier name.
- Variant tokens embedded in variables or function calls (leave those variables unchanged, or store already-prefixed class names).
## Chained modifiers
@@ -69,7 +68,7 @@ class:sm:hover="underline"
- Put base utilities on `class` / `className`.
- Vue / Blade / HTML: use `class:modifier="…"`.
-- React: prefer `className:modifier="…"` for static variants. For runtime conditions that still use string literals, `className:modifier={cond ? 'a' : 'b'}` is valid and will prefix those literals. Leave `className={…}` base expressions unchanged when they are unrelated.
+- React: prefer `className:modifier="…"` for static variants, including `className:@md`, `className:group-hover/item`, `className:[&>*]`, and `className:data-[state=open]` (UseClassy rewrites before JSX parse). For runtime conditions that still use string literals, `className:modifier={cond ? 'a' : 'b'}` is valid and will prefix those literals. Leave `className={…}` base expressions unchanged when they are unrelated.
- Vue: leave `:class` and other dynamic bindings unchanged.
- **Svelte**: only transform quoted UseClassy modifiers. Native `class:active={cond}` and `class:active` stay untouched — do not rewrite those.
- Do not move conditional base utilities into modifier attributes on Vue/Svelte/Blade; UseClassy modifiers represent Tailwind variants. React is the exception for `className:mod={…}` expression values.