feat(compiler): configurable @property handling for --tw-* slots

Tailwind registers internal slots (`--tw-content`, `--tw-shadow`, …) via
`@property` rules that supply their defaults. `decompose` dropped those
rules, so utilities that reference a slot without setting it locally —
notably `after:*`/`before:*` emitting `content: var(--tw-content)` — left
a dangling reference that resolves to nothing and suppresses the
pseudo-element.

`decompose` now captures the `@property` registrations, and `emitCss`
gains a `properties` option to handle them, configurably:

  - `mode: 'emit'`   — emit `@property` rules for referenced slots,
    preserving Tailwind's typed defaults.
  - `mode: 'inline'` — substitute each slot's `initial-value` into the
    values that reference it (a superset of `inlineVars` for the matched
    slots), so the output is fully self-contained.
  - `resolve(name, captured)` hook — override or supply a slot's config
    (initial-value / syntax / inherits), or register one Tailwind didn't.
  - `match` — restrict handling to matching names (default `/^--tw-/`).

`tailwindPlugin` forwards `properties` through its `onCss` path. Adds 9
tests (capture, emit, inline, resolve override, match, back-compat,
plugin e2e). Compiler suite: 178 passing.
This commit is contained in:
Rahim
2026-06-17 17:13:40 -07:00
parent fbe5ffeea3
commit 0486765848
8 changed files with 341 additions and 18 deletions
@@ -464,4 +464,27 @@ function App(){ return <PlayButton className={iconButton}/>; }`;
const css = await cssPromise;
expect(css).toMatch(/\[data-skin="x"\]\s*{[^}]*--spacing:/);
});
it('forwards the `properties` option (inline) so --tw-content resolves', async () => {
// `after:absolute` emits `content: var(--tw-content)` with no setter.
const source = `function App(){ return <Foo className="after:absolute"/>; }`;
const cssPromise = new Promise<string>((resolve) => {
compile(source, {
target: 'react',
plugins: [
tailwindPlugin({
design,
target: 'vanilla-css',
properties: { mode: 'inline' },
onCss: (out) => {
if (out.kind === 'merged') resolve(out.css);
},
}),
],
});
});
const css = await cssPromise;
expect(css).not.toMatch(/var\(--tw-content\)/);
expect(collapse(css)).toContain(collapse('content: "";'));
});
});