Skip to content

Commit b108906

Browse files
authored
feat(preset-wind4): custom property output backup (#4950)
1 parent 42e6327 commit b108906

File tree

7 files changed

+193
-37
lines changed

7 files changed

+193
-37
lines changed

docs/presets/wind4.md

Lines changed: 88 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -212,6 +212,94 @@ export default defineConfig({
212212
})
213213
```
214214

215+
#### Property
216+
217+
Control the generation of `@property` CSS rules in the `properties` layer.
218+
219+
By default, PresetWind4 uses `@property` to define CSS custom properties for better browser optimization. These properties are automatically generated based on your utilities usage and wrapped in a `@supports` query for progressive enhancement.
220+
221+
```ts twoslash [uno.config.ts]
222+
import { defineConfig, presetWind4 } from 'unocss'
223+
224+
export default defineConfig({
225+
presets: [
226+
presetWind4({
227+
preflights: {
228+
property: true, // Enable (default) | `false` to disable [!code ++]
229+
},
230+
}),
231+
],
232+
})
233+
```
234+
235+
##### Parent and Selector
236+
237+
You can customize the parent wrapper and selector:
238+
239+
```ts twoslash [uno.config.ts]
240+
import { defineConfig, presetWind4 } from 'unocss'
241+
242+
export default defineConfig({
243+
presets: [
244+
presetWind4({
245+
preflights: {
246+
property: {
247+
// Custom parent selector (e.g., use @layer instead of @supports)
248+
parent: '@layer custom-properties',
249+
// Custom selector for applying properties
250+
selector: ':where(*, ::before, ::after)',
251+
},
252+
},
253+
}),
254+
],
255+
})
256+
```
257+
258+
If you don't want the `@supports` wrapper and want properties applied directly:
259+
260+
```ts twoslash [uno.config.ts]
261+
import { defineConfig, presetWind4 } from 'unocss'
262+
263+
export default defineConfig({
264+
presets: [
265+
presetWind4({
266+
preflights: {
267+
property: {
268+
parent: false, // No parent wrapper
269+
},
270+
},
271+
}),
272+
],
273+
})
274+
```
275+
276+
**Default output:**
277+
278+
```css
279+
@supports ((-webkit-hyphens: none) and (not (margin-trim: inline))) or
280+
((-moz-orient: inline) and (not (color: rgb(from red r g b)))) {
281+
*,
282+
::before,
283+
::after,
284+
::backdrop {
285+
--un-text-opacity: 100%;
286+
/* ... */
287+
}
288+
}
289+
```
290+
291+
**With `parent: false`:**
292+
293+
```css
294+
*,
295+
::before,
296+
::after,
297+
::backdrop {
298+
--un-text-opacity: 100%;
299+
/* ... */
300+
}
301+
```
302+
215303
## Generated CSS
216304

217305
In the output of PresetWind4, three new layers have been added: `base`, `theme`, and `properties`.

packages-presets/preset-wind4/src/index.ts

Lines changed: 26 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -120,6 +120,32 @@ export interface PresetWind4Options extends PresetOptions {
120120
* to match the design system or requirements of your project.
121121
*/
122122
theme?: PreflightsTheme['mode'] | PreflightsTheme
123+
124+
/**
125+
* Configuration for property preflight generation.
126+
*
127+
* - `false`: Disable property preflight
128+
* - `true` or `undefined`: Enable with default configuration
129+
* - `object`: Enable with custom configuration
130+
*/
131+
property?: boolean | {
132+
/**
133+
* Custom parent selector (e.g., @supports query or @layer).
134+
*
135+
* - `string`: Use custom parent selector
136+
* - `false`: No parent wrapper, apply properties directly to selector
137+
* - `undefined`: Use default @supports query
138+
*
139+
* @default '@supports ((-webkit-hyphens: none) and (not (margin-trim: inline))) or ((-moz-orient: inline) and (not (color:rgb(from red r g b))))'
140+
*/
141+
parent?: string | false
142+
/**
143+
* Custom selector for applying properties.
144+
*
145+
* @default '*, ::before, ::after, ::backdrop'
146+
*/
147+
selector?: string
148+
}
123149
}
124150
}
125151

@@ -129,15 +155,6 @@ export const presetWind4 = definePreset<PresetWind4Options, Theme>((options = {}
129155
options.variablePrefix = options.variablePrefix ?? 'un-'
130156
options.important = options.important ?? false
131157

132-
const preflightsTheme = (typeof options.preflights?.theme === 'boolean' || typeof options.preflights?.theme === 'string')
133-
? { mode: options.preflights.theme ?? 'on-demand' }
134-
: { mode: options.preflights?.theme?.mode ?? 'on-demand', ...options.preflights?.theme }
135-
136-
options.preflights = {
137-
reset: options.preflights?.reset ?? true,
138-
theme: preflightsTheme,
139-
}
140-
141158
return {
142159
name: PRESET_NAME,
143160
rules,
Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,14 @@
11
import type { Preflight } from '@unocss/core'
22
import type { PresetWind4Options } from '..'
33
import type { Theme } from '../theme/types'
4-
import { properties } from './properties'
4+
import { property } from './property'
55
import { reset } from './reset'
66
import { theme } from './theme'
77

88
export const preflights: (options: PresetWind4Options) => Preflight<Theme>[] = (options) => {
99
return [
1010
reset(options),
1111
theme(options),
12-
properties(),
12+
property(options),
1313
].filter(Boolean) as Preflight<Theme>[]
1414
}

packages-presets/preset-wind4/src/preflights/properties.ts

Lines changed: 0 additions & 20 deletions
This file was deleted.
Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
import type { Preflight } from '@unocss/core'
2+
import type { PresetWind4Options } from '..'
3+
import type { Theme } from '../theme'
4+
import { trackedProperties } from '../utils/track'
5+
6+
export function property(options: PresetWind4Options): Preflight<Theme> | undefined {
7+
if (options.preflights?.property === false)
8+
return undefined
9+
10+
const propertyConfig = typeof options.preflights?.property === 'object'
11+
? options.preflights.property
12+
: undefined
13+
14+
const parentSelector = propertyConfig?.parent !== undefined
15+
? propertyConfig.parent
16+
: '@supports ((-webkit-hyphens: none) and (not (margin-trim: inline))) or ((-moz-orient: inline) and (not (color:rgb(from red r g b))))'
17+
18+
const selector = propertyConfig?.selector
19+
?? '*, ::before, ::after, ::backdrop'
20+
21+
return {
22+
getCSS: () => {
23+
if (trackedProperties.size === 0)
24+
return
25+
26+
const css = Array.from(trackedProperties.entries())
27+
.map(([property, value]) => `${property}:${value};`)
28+
.join('')
29+
30+
return parentSelector === false
31+
? `${selector}{${css}}`
32+
: `${parentSelector}{${selector}{${css}}}`
33+
},
34+
layer: 'properties',
35+
}
36+
}

packages-presets/preset-wind4/src/preflights/theme.ts

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -53,12 +53,16 @@ function getThemeVarsMap(theme: Theme, keys: string[]): Map<string, string> {
5353
}
5454

5555
export function theme(options: PresetWind4Options): Preflight<Theme> {
56+
const preflightsTheme: PreflightsTheme = (typeof options.preflights?.theme === 'boolean' || typeof options.preflights?.theme === 'string')
57+
? { mode: options.preflights.theme ?? 'on-demand' }
58+
: { mode: options.preflights?.theme?.mode ?? 'on-demand', ...options.preflights?.theme }
59+
5660
return {
5761
layer: 'theme',
5862
getCSS(ctx) {
5963
const { theme, generator } = ctx
6064
const safelist = uniq(generator.config.safelist.flatMap(s => typeof s === 'function' ? s(ctx) : s))
61-
const { mode, process } = options.preflights!.theme as PreflightsTheme
65+
const { mode, process } = preflightsTheme
6266
if (mode === false) {
6367
return undefined
6468
}

test/preset-wind4.test.ts

Lines changed: 36 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -487,7 +487,7 @@ describe('important', () => {
487487
'dark:bg-blue',
488488
].join(' '), { preflights: false })
489489

490-
await expect(css).toMatchFileSnapshot('./assets/output/preset-wind4-important-true.css')
490+
expect(css).toMatchFileSnapshot('./assets/output/preset-wind4-important-true.css')
491491
})
492492

493493
it(`should prefix selector with provided important string and wrap the original selector in ":is()"`, async () => {
@@ -508,7 +508,7 @@ describe('important', () => {
508508
'selection:bg-yellow',
509509
].join(' '), { preflights: false })
510510

511-
await expect(css).toMatchFileSnapshot('./assets/output/preset-wind4-important-string.css')
511+
expect(css).toMatchFileSnapshot('./assets/output/preset-wind4-important-string.css')
512512
})
513513

514514
it('shadow with opacity', async () => {
@@ -527,7 +527,7 @@ describe('important', () => {
527527
'shadow-red-300/30',
528528
].join(' '), { preflights: false })
529529

530-
await expect(css).toMatchInlineSnapshot(`
530+
expect(css).toMatchInlineSnapshot(`
531531
"/* layer: properties */
532532
@property --un-inset-ring-color{syntax:"*";inherits:false;}
533533
@property --un-inset-ring-shadow{syntax:"*";inherits:false;initial-value:0 0 #0000;}
@@ -570,7 +570,7 @@ describe('important', () => {
570570
'text-shadow-red-300/30',
571571
].join(' '), { preflights: false })
572572

573-
await expect(css).toMatchInlineSnapshot(`
573+
expect(css).toMatchInlineSnapshot(`
574574
"/* layer: properties */
575575
@property --un-text-shadow-opacity{syntax:"<percentage>";inherits:false;initial-value:100%;}
576576
/* layer: default */
@@ -601,7 +601,7 @@ describe('important', () => {
601601
'drop-shadow-red-300/30',
602602
].join(' '), { preflights: false })
603603

604-
await expect(css).toMatchInlineSnapshot(`
604+
expect(css).toMatchInlineSnapshot(`
605605
"/* layer: properties */
606606
@property --un-blur{syntax:"*";inherits:false;}
607607
@property --un-brightness{syntax:"*";inherits:false;}
@@ -626,4 +626,35 @@ describe('important', () => {
626626
}"
627627
`)
628628
})
629+
630+
it('custom properties', async () => {
631+
const uno = await createGenerator({
632+
presets: [
633+
presetWind4({
634+
preflights: {
635+
reset: false,
636+
property: {
637+
parent: false,
638+
selector: ':host',
639+
},
640+
},
641+
}),
642+
],
643+
})
644+
645+
const { css } = await uno.generate('text-red')
646+
647+
expect(css).toMatchInlineSnapshot(`
648+
"/* layer: properties */
649+
:host{--un-text-opacity:100%;}
650+
@property --un-text-opacity{syntax:"<percentage>";inherits:false;initial-value:100%;}
651+
/* layer: theme */
652+
:root, :host { --colors-red-DEFAULT: oklch(70.4% 0.191 22.216); }
653+
/* layer: default */
654+
.text-red{color:color-mix(in srgb, var(--colors-red-DEFAULT) var(--un-text-opacity), transparent);}
655+
@supports (color: color-mix(in lab, red, red)){
656+
.text-red{color:color-mix(in oklab, var(--colors-red-DEFAULT) var(--un-text-opacity), transparent);}
657+
}"
658+
`)
659+
})
629660
})

0 commit comments

Comments
 (0)