diff --git a/packages/apollo-wind/src/index.ts b/packages/apollo-wind/src/index.ts index 12d90f7d3..1bbcc02f1 100644 --- a/packages/apollo-wind/src/index.ts +++ b/packages/apollo-wind/src/index.ts @@ -6,6 +6,7 @@ // Utilities // ----------------------------------------------------------------------------- export { cn } from './lib/utils'; +export { registerCssPropertyRules } from './lib/register-shadow-dom-properties'; // ----------------------------------------------------------------------------- // Layout Components diff --git a/packages/apollo-wind/src/lib/index.ts b/packages/apollo-wind/src/lib/index.ts index e85b6f5ae..b4929a789 100644 --- a/packages/apollo-wind/src/lib/index.ts +++ b/packages/apollo-wind/src/lib/index.ts @@ -1 +1,2 @@ export { cn, get, deepEqual } from './utils'; +export { registerCssPropertyRules } from './register-shadow-dom-properties'; diff --git a/packages/apollo-wind/src/lib/register-shadow-dom-properties.test.ts b/packages/apollo-wind/src/lib/register-shadow-dom-properties.test.ts new file mode 100644 index 000000000..ce6f51b92 --- /dev/null +++ b/packages/apollo-wind/src/lib/register-shadow-dom-properties.test.ts @@ -0,0 +1,58 @@ +import { afterEach, beforeEach, describe, expect, it } from 'vitest'; +import { registerCssPropertyRules } from './register-shadow-dom-properties'; + +const SELECTOR = 'style[data-tw-property-rules]'; + +const SAMPLE_CSS = ` +@layer base { .border { border-style: var(--tw-border-style); border-width: 1px; } } +@property --tw-border-style{syntax:"*";inherits:false;initial-value:solid} +@property --tw-shadow{syntax:"*";inherits:false;initial-value:0 0 #0000} +@property --tw-translate-x{syntax:"*";inherits:false;initial-value:0} +.bg-popover { background-color: var(--popover); } +`; + +beforeEach(() => { + document.querySelectorAll(SELECTOR).forEach((el) => el.remove()); +}); + +afterEach(() => { + document.querySelectorAll(SELECTOR).forEach((el) => el.remove()); +}); + +describe('registerCssPropertyRules', () => { + it('extracts @property rules from CSS and injects them into document.head', () => { + registerCssPropertyRules(SAMPLE_CSS); + + const style = document.querySelector(SELECTOR); + expect(style).not.toBeNull(); + + const text = style?.textContent ?? ''; + expect(text).toContain('@property --tw-border-style'); + expect(text).toContain('@property --tw-shadow'); + expect(text).toContain('@property --tw-translate-x'); + }); + + it('does not include non-@property rules', () => { + registerCssPropertyRules(SAMPLE_CSS); + + const text = document.querySelector(SELECTOR)?.textContent ?? ''; + expect(text).not.toContain('.border'); + expect(text).not.toContain('.bg-popover'); + expect(text).not.toContain('@layer'); + }); + + it('injects only one style element when called multiple times', () => { + registerCssPropertyRules(SAMPLE_CSS); + registerCssPropertyRules(SAMPLE_CSS); + registerCssPropertyRules(SAMPLE_CSS); + + const elements = document.querySelectorAll(SELECTOR); + expect(elements).toHaveLength(1); + }); + + it('does nothing when CSS has no @property rules', () => { + registerCssPropertyRules('.flex { display: flex; }'); + + expect(document.querySelector(SELECTOR)).toBeNull(); + }); +}); diff --git a/packages/apollo-wind/src/lib/register-shadow-dom-properties.ts b/packages/apollo-wind/src/lib/register-shadow-dom-properties.ts new file mode 100644 index 000000000..3dd489ab8 --- /dev/null +++ b/packages/apollo-wind/src/lib/register-shadow-dom-properties.ts @@ -0,0 +1,50 @@ +/** + * Register Tailwind v4 `@property` CSS declarations at the document level. + * + * Tailwind v4 emits `@property` at-rules (e.g. `@property --tw-border-style`) + * that define initial values for intermediate custom properties used by + * composable utilities (border, shadow, ring, transform). Browsers register + * `@property` rules globally — but silently **ignore** them when they appear + * inside a shadow root's `