@wix/zero-config-implementation 1.66.0 → 1.68.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/dist/{index-BEdi2EUy.js → index-BewAGOQY.js} +13017 -12847
- package/dist/{index-CtX9f415.js → index-DTB9iwv9.js} +1 -1
- package/dist/index.d.ts +27 -0
- package/dist/index.js +1 -1
- package/package.json +2 -2
- package/src/converters/custom-states-builder.test.ts +137 -0
- package/src/converters/custom-states-builder.ts +117 -0
- package/src/converters/to-editor-component.ts +53 -3
- package/src/information-extractors/css/parse.test.ts +122 -0
- package/src/information-extractors/css/parse.ts +177 -1
- package/src/information-extractors/css/types.ts +27 -0
- package/src/information-extractors/ts/components.test.ts +29 -0
- package/src/information-extractors/ts/components.ts +72 -3
- package/src/information-extractors/ts/types.ts +5 -0
- package/src/utils/css-class.ts +10 -0
package/dist/index.d.ts
CHANGED
|
@@ -422,6 +422,12 @@ export declare interface CSSParserAPI {
|
|
|
422
422
|
* @param defaultValue - The initial value string of the custom property
|
|
423
423
|
*/
|
|
424
424
|
getVarPropertyType: (varName: string, defaultValue: string) => string | undefined;
|
|
425
|
+
/**
|
|
426
|
+
* Returns every `:global(.modifier)` design-state class found in the CSS, paired
|
|
427
|
+
* with any native pseudo-class on the same base element within the same rule.
|
|
428
|
+
* Modifiers with no paired pseudo are custom (non-native) state triggers.
|
|
429
|
+
*/
|
|
430
|
+
getStateClasses: () => StateClassRule[];
|
|
425
431
|
}
|
|
426
432
|
|
|
427
433
|
export declare interface CssPropertiesData {
|
|
@@ -1577,6 +1583,8 @@ PropsTwo extends ErrorProps,
|
|
|
1577
1583
|
declare type MethodOptions<PluginArg extends Plugin> =
|
|
1578
1584
|
ExternalPluginOptions<PluginArg>
|
|
1579
1585
|
|
|
1586
|
+
declare type NativePseudoClass = 'hover' | 'focus' | 'disabled' | 'invalid';
|
|
1587
|
+
|
|
1580
1588
|
/**
|
|
1581
1589
|
* Normalize each error in the `errors` option to `Error` instances
|
|
1582
1590
|
*/
|
|
@@ -2356,6 +2364,7 @@ export declare interface PropInfo {
|
|
|
2356
2364
|
resolvedType: ResolvedType;
|
|
2357
2365
|
description?: string;
|
|
2358
2366
|
deprecated?: boolean;
|
|
2367
|
+
isStateTrigger?: boolean;
|
|
2359
2368
|
}
|
|
2360
2369
|
|
|
2361
2370
|
export declare interface PropSpyMeta {
|
|
@@ -2644,6 +2653,24 @@ CauseArg extends Cause,
|
|
|
2644
2653
|
> = MainInstanceOptions<AggregateErrorsArg, CauseArg> &
|
|
2645
2654
|
PluginsOptions<PluginsArg, ChildProps>
|
|
2646
2655
|
|
|
2656
|
+
/**
|
|
2657
|
+
* A `:global(.modifier)` design-state class found on a base element selector.
|
|
2658
|
+
* Produced by walking each CSS rule and pairing the global modifier with any
|
|
2659
|
+
* native pseudo-class present on the same base within that rule.
|
|
2660
|
+
*/
|
|
2661
|
+
declare interface StateClassRule {
|
|
2662
|
+
/** DOM-matchable base selector (e.g. `.root`), used to map the modifier to an element. */
|
|
2663
|
+
baseSelector: string;
|
|
2664
|
+
/** Global modifier class name from `:global(.X)`, without the leading dot. */
|
|
2665
|
+
modifier: string;
|
|
2666
|
+
/**
|
|
2667
|
+
* Native pseudo-class paired with this modifier on the same base within the same
|
|
2668
|
+
* rule. When present, the modifier is the editor-trigger class for a native state;
|
|
2669
|
+
* when absent, the modifier denotes a custom (non-native) design state.
|
|
2670
|
+
*/
|
|
2671
|
+
pseudoClass?: NativePseudoClass;
|
|
2672
|
+
}
|
|
2673
|
+
|
|
2647
2674
|
/**
|
|
2648
2675
|
* Unbound static method of a plugin
|
|
2649
2676
|
*/
|
package/dist/index.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { B as t, D as e, E as o, I as n, N as c, P as l, R as i, a as p, V as E, b as m, c as x, d as f, e as d, f as u, h as C, i as I, j as k, k as y, l as A, m as D, n as P, o as R, p as h, q as B, r as M, s as T, t as b, w } from "./index-
|
|
1
|
+
import { B as t, D as e, E as o, I as n, N as c, P as l, R as i, a as p, V as E, b as m, c as x, d as f, e as d, f as u, h as C, i as I, j as k, k as y, l as A, m as D, n as P, o as R, p as h, q as B, r as M, s as T, t as b, w } from "./index-BewAGOQY.js";
|
|
2
2
|
import "react";
|
|
3
3
|
export {
|
|
4
4
|
t as BaseError,
|
package/package.json
CHANGED
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
"registry": "https://registry.npmjs.org/",
|
|
5
5
|
"access": "public"
|
|
6
6
|
},
|
|
7
|
-
"version": "1.
|
|
7
|
+
"version": "1.68.0",
|
|
8
8
|
"description": "Core library for extracting component manifests from JS and CSS files",
|
|
9
9
|
"type": "module",
|
|
10
10
|
"main": "dist/index.js",
|
|
@@ -81,5 +81,5 @@
|
|
|
81
81
|
]
|
|
82
82
|
}
|
|
83
83
|
},
|
|
84
|
-
"falconPackageHash": "
|
|
84
|
+
"falconPackageHash": "5848ef0051ff536666dd46c6264f765b40f280afa13b5d120028a1a2"
|
|
85
85
|
}
|
|
@@ -0,0 +1,137 @@
|
|
|
1
|
+
import { describe, expect, it } from 'vitest'
|
|
2
|
+
import type { ExtractedElement } from '../information-extractors/react'
|
|
3
|
+
import type { CoupledProp } from '../information-extractors/react/types'
|
|
4
|
+
import { type CustomClassTrigger, buildCustomStatesBlock } from './custom-states-builder'
|
|
5
|
+
|
|
6
|
+
function elementWithClass(classAttribute: string): ExtractedElement {
|
|
7
|
+
return { attributes: { class: classAttribute } } as unknown as ExtractedElement
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
function booleanStateProp(name: string): CoupledProp {
|
|
11
|
+
return { name, type: 'boolean', isStateTrigger: true } as unknown as CoupledProp
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
function classTrigger(baseSelector: string, modifier: string, isCssModule = false): CustomClassTrigger {
|
|
15
|
+
return { baseSelector, modifier, isCssModule }
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
describe('buildCustomStatesBlock', () => {
|
|
19
|
+
it('emits a custom-class state with className only (no props, no pseudoClass)', () => {
|
|
20
|
+
const states = buildCustomStatesBlock({
|
|
21
|
+
element: elementWithClass('custom-states'),
|
|
22
|
+
propTriggers: [],
|
|
23
|
+
classTriggers: [classTrigger('.custom-states', 'custom-states--featured')],
|
|
24
|
+
})
|
|
25
|
+
|
|
26
|
+
expect(states).toEqual({ featured: { displayName: 'Featured', className: 'custom-states--featured' } })
|
|
27
|
+
})
|
|
28
|
+
|
|
29
|
+
it('derives a multi-word state name from the modifier and capital-cases the displayName', () => {
|
|
30
|
+
const states = buildCustomStatesBlock({
|
|
31
|
+
element: elementWithClass('widget'),
|
|
32
|
+
propTriggers: [],
|
|
33
|
+
classTriggers: [classTrigger('.widget', 'widget--in-progress')],
|
|
34
|
+
})
|
|
35
|
+
|
|
36
|
+
expect(states?.['in-progress']).toEqual({
|
|
37
|
+
displayName: 'In Progress',
|
|
38
|
+
className: 'widget--in-progress',
|
|
39
|
+
})
|
|
40
|
+
})
|
|
41
|
+
|
|
42
|
+
it('does not apply a block-base class trigger to a BEM child element (plain CSS, exact match)', () => {
|
|
43
|
+
const states = buildCustomStatesBlock({
|
|
44
|
+
element: elementWithClass('custom-states__label'),
|
|
45
|
+
propTriggers: [],
|
|
46
|
+
classTriggers: [classTrigger('.custom-states', 'custom-states--featured')],
|
|
47
|
+
})
|
|
48
|
+
|
|
49
|
+
expect(states).toBeUndefined()
|
|
50
|
+
})
|
|
51
|
+
|
|
52
|
+
it('matches a base class rendered as a double-underscore CSS-module hash (`root__HASH`)', () => {
|
|
53
|
+
// Vite emits `[name]__[hash]` for `styles.root`; the hash carries uppercase, so it is the
|
|
54
|
+
// base element — not a BEM child. (Regression: this used to be dropped as a child.)
|
|
55
|
+
const states = buildCustomStatesBlock({
|
|
56
|
+
element: elementWithClass('root__IWjkn wixui-test-comp-card test-comp-card'),
|
|
57
|
+
propTriggers: [],
|
|
58
|
+
classTriggers: [classTrigger('.root', 'test-comp-card--featured', true)],
|
|
59
|
+
})
|
|
60
|
+
|
|
61
|
+
expect(states).toEqual({ featured: { displayName: 'Featured', className: 'test-comp-card--featured' } })
|
|
62
|
+
})
|
|
63
|
+
|
|
64
|
+
it('still excludes a genuine BEM child (`root__label`) from a block-base trigger in a module', () => {
|
|
65
|
+
const states = buildCustomStatesBlock({
|
|
66
|
+
element: elementWithClass('root__label'),
|
|
67
|
+
propTriggers: [],
|
|
68
|
+
classTriggers: [classTrigger('.root', 'test-comp-card--featured', true)],
|
|
69
|
+
})
|
|
70
|
+
|
|
71
|
+
expect(states).toBeUndefined()
|
|
72
|
+
})
|
|
73
|
+
|
|
74
|
+
it('returns undefined when no trigger targets the element', () => {
|
|
75
|
+
const states = buildCustomStatesBlock({
|
|
76
|
+
element: elementWithClass('custom-states'),
|
|
77
|
+
propTriggers: [],
|
|
78
|
+
classTriggers: [],
|
|
79
|
+
})
|
|
80
|
+
|
|
81
|
+
expect(states).toBeUndefined()
|
|
82
|
+
})
|
|
83
|
+
|
|
84
|
+
it('emits a props-only state for a prop trigger with no matching class (no className synthesized)', () => {
|
|
85
|
+
const states = buildCustomStatesBlock({
|
|
86
|
+
element: elementWithClass('panel'),
|
|
87
|
+
propTriggers: [booleanStateProp('isExpanded')],
|
|
88
|
+
classTriggers: [],
|
|
89
|
+
})
|
|
90
|
+
|
|
91
|
+
expect(states).toEqual({
|
|
92
|
+
'is-expanded': { displayName: 'Is Expanded', props: { isExpanded: true } },
|
|
93
|
+
})
|
|
94
|
+
})
|
|
95
|
+
|
|
96
|
+
it('keeps the full prop name in the state key and displayName (no is/has stripping)', () => {
|
|
97
|
+
const states = buildCustomStatesBlock({
|
|
98
|
+
element: elementWithClass('widget'),
|
|
99
|
+
propTriggers: [booleanStateProp('isLoading')],
|
|
100
|
+
classTriggers: [],
|
|
101
|
+
})
|
|
102
|
+
|
|
103
|
+
expect(states?.['is-loading']).toEqual({
|
|
104
|
+
displayName: 'Is Loading',
|
|
105
|
+
props: { isLoading: true },
|
|
106
|
+
})
|
|
107
|
+
})
|
|
108
|
+
|
|
109
|
+
it('merges a prop trigger with a class trigger that resolves to the same state name', () => {
|
|
110
|
+
const states = buildCustomStatesBlock({
|
|
111
|
+
element: elementWithClass('prop-states'),
|
|
112
|
+
propTriggers: [booleanStateProp('isLoading')],
|
|
113
|
+
classTriggers: [classTrigger('.prop-states', 'prop-states--is-loading')],
|
|
114
|
+
})
|
|
115
|
+
|
|
116
|
+
expect(states).toEqual({
|
|
117
|
+
'is-loading': {
|
|
118
|
+
displayName: 'Is Loading',
|
|
119
|
+
className: 'prop-states--is-loading',
|
|
120
|
+
props: { isLoading: true },
|
|
121
|
+
},
|
|
122
|
+
})
|
|
123
|
+
})
|
|
124
|
+
|
|
125
|
+
it('ignores plain (non-ElementState) props and non-boolean state triggers', () => {
|
|
126
|
+
const states = buildCustomStatesBlock({
|
|
127
|
+
element: elementWithClass('custom-states'),
|
|
128
|
+
propTriggers: [
|
|
129
|
+
{ name: 'showLabel', type: 'boolean' } as unknown as CoupledProp,
|
|
130
|
+
{ name: 'variant', type: "'a' | 'b'", isStateTrigger: true } as unknown as CoupledProp,
|
|
131
|
+
],
|
|
132
|
+
classTriggers: [],
|
|
133
|
+
})
|
|
134
|
+
|
|
135
|
+
expect(states).toBeUndefined()
|
|
136
|
+
})
|
|
137
|
+
})
|
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Builds the custom (non-native) `states` entries for an element.
|
|
3
|
+
*
|
|
4
|
+
* Two triggers feed this, per the agreed design — props are looked at first:
|
|
5
|
+
* - Prop — a boolean prop wrapped in `ElementState<>` (TS extractor sets
|
|
6
|
+
* `isStateTrigger`) IS a custom state, emitting
|
|
7
|
+
* `props: { <prop>: true }`. The CSS is then searched for a
|
|
8
|
+
* class trigger matching that state's name — when found it
|
|
9
|
+
* supplies the editor-trigger `className`; otherwise the entry
|
|
10
|
+
* is props-only (`className` is never synthesized).
|
|
11
|
+
* - Custom class — a `:global(.modifier)` class on the element with no paired
|
|
12
|
+
* native pseudo (CSS extractor `getStateClasses`) and not
|
|
13
|
+
* claimed by a prop trigger. Emits the modifier as a
|
|
14
|
+
* className-only state.
|
|
15
|
+
*
|
|
16
|
+
* Native states are built separately and take precedence on any key collision.
|
|
17
|
+
*/
|
|
18
|
+
|
|
19
|
+
import type { States } from '@wix/react-component-schema'
|
|
20
|
+
import { kebabCase } from 'case-anything'
|
|
21
|
+
import type { StateClassRule } from '../information-extractors/css/types'
|
|
22
|
+
import type { CoupledProp, ExtractedElement } from '../information-extractors/react'
|
|
23
|
+
import { isGlobalSemanticClass, stateNameFromModifier } from '../utils/css-class'
|
|
24
|
+
import { formatDisplayName } from './utils'
|
|
25
|
+
|
|
26
|
+
/** A custom-class trigger tagged with whether its source file is a CSS module. */
|
|
27
|
+
export type CustomClassTrigger = StateClassRule & { isCssModule: boolean }
|
|
28
|
+
|
|
29
|
+
interface BuildCustomStatesParams {
|
|
30
|
+
element: ExtractedElement
|
|
31
|
+
/** Prop triggers in scope for this element (root-level `ElementState<>` props; `[]` for inner elements). */
|
|
32
|
+
propTriggers: CoupledProp[]
|
|
33
|
+
/** Custom-class triggers (modifiers with no paired native pseudo) across all CSS files. */
|
|
34
|
+
classTriggers: CustomClassTrigger[]
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
export function buildCustomStatesBlock({
|
|
38
|
+
element,
|
|
39
|
+
propTriggers,
|
|
40
|
+
classTriggers,
|
|
41
|
+
}: BuildCustomStatesParams): States | undefined {
|
|
42
|
+
const states: States = {}
|
|
43
|
+
|
|
44
|
+
// Class triggers that target this element, keyed by their derived state name,
|
|
45
|
+
// so prop triggers can look up their matching class.
|
|
46
|
+
const elementClassTriggers = new Map<string, CustomClassTrigger>()
|
|
47
|
+
for (const rule of classTriggers) {
|
|
48
|
+
if (!elementMatchesBase(element, rule.baseSelector, rule.isCssModule)) continue
|
|
49
|
+
elementClassTriggers.set(stateNameFromModifier(rule.modifier), rule)
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
// Prop triggers first: each boolean `ElementState<>` prop is a custom state.
|
|
53
|
+
// Then search the CSS for the class trigger matching that state's name — when
|
|
54
|
+
// found it supplies the editor-trigger className; otherwise props-only.
|
|
55
|
+
for (const prop of propTriggers) {
|
|
56
|
+
if (!prop.isStateTrigger || prop.type !== 'boolean') continue
|
|
57
|
+
const stateName = stateNameFromProp(prop.name)
|
|
58
|
+
const matchingClassTrigger = elementClassTriggers.get(stateName)
|
|
59
|
+
states[stateName] = {
|
|
60
|
+
displayName: formatDisplayName(stateName),
|
|
61
|
+
...(matchingClassTrigger && { className: matchingClassTrigger.modifier }),
|
|
62
|
+
props: { [prop.name]: true },
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
// Remaining class triggers — those not claimed by a prop — are className-only states.
|
|
67
|
+
for (const [stateName, rule] of elementClassTriggers) {
|
|
68
|
+
if (states[stateName] !== undefined) continue
|
|
69
|
+
states[stateName] = {
|
|
70
|
+
displayName: formatDisplayName(stateName),
|
|
71
|
+
className: rule.modifier,
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
return Object.keys(states).length > 0 ? states : undefined
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
/**
|
|
79
|
+
* Derives the state name from a boolean prop by kebab-casing the full prop name
|
|
80
|
+
* (`isLoading` → `is-loading`, so the displayName reads "Is Loading"). The prop
|
|
81
|
+
* name is kept whole — no `is`/`has` prefix stripping — so the state key mirrors
|
|
82
|
+
* the prop that triggers it.
|
|
83
|
+
*/
|
|
84
|
+
function stateNameFromProp(propName: string): string {
|
|
85
|
+
return kebabCase(propName)
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
/**
|
|
89
|
+
* Checks whether an element's rendered class list matches a simple single-class
|
|
90
|
+
* base selector (e.g. `.root`, `.custom-states`).
|
|
91
|
+
*
|
|
92
|
+
* Exact class match always counts. For CSS-module files the Vite hash patterns also
|
|
93
|
+
* count: `localName_HASH`, `_localName_HASH`, and the double-underscore `localName__HASH`
|
|
94
|
+
* form. The catch is that `localName__HASH` is syntactically identical to a BEM child
|
|
95
|
+
* class (`localName__child`) — which denotes a *different* element and must NOT match the
|
|
96
|
+
* block's base selector. We disambiguate by shape: a real BEM child is an all-lowercase
|
|
97
|
+
* semantic name (`isGlobalSemanticClass`), whereas a module hash carries uppercase / other
|
|
98
|
+
* non-kebab characters. So only genuine BEM children are excluded; hashed base classes match.
|
|
99
|
+
*/
|
|
100
|
+
function elementMatchesBase(element: ExtractedElement, baseSelector: string, isCssModule: boolean): boolean {
|
|
101
|
+
const match = baseSelector.match(/^\.([\w-]+)$/)
|
|
102
|
+
if (!match) return false
|
|
103
|
+
|
|
104
|
+
const localName = match[1]
|
|
105
|
+
const classList = element.attributes.class?.split(/\s+/).filter(Boolean) ?? []
|
|
106
|
+
return classList.some((className) => {
|
|
107
|
+
if (className === localName) return true
|
|
108
|
+
if (!isCssModule) return false
|
|
109
|
+
// Genuine BEM child (`localName__<lowercase-element>`) → a different element, exclude it.
|
|
110
|
+
// A double-underscore module hash (`localName__<hash>`) is NOT semantic, so it falls through.
|
|
111
|
+
const isBemChild =
|
|
112
|
+
(className.startsWith(`${localName}__`) || className.startsWith(`_${localName}__`)) &&
|
|
113
|
+
isGlobalSemanticClass(className.replace(/^_/, ''))
|
|
114
|
+
if (isBemChild) return false
|
|
115
|
+
return className.startsWith(`${localName}_`) || className.startsWith(`_${localName}_`)
|
|
116
|
+
})
|
|
117
|
+
}
|
|
@@ -6,6 +6,7 @@ import type {
|
|
|
6
6
|
EditorElement,
|
|
7
7
|
EditorReactComponent,
|
|
8
8
|
ElementItem,
|
|
9
|
+
States,
|
|
9
10
|
} from '@wix/react-component-schema'
|
|
10
11
|
import { CSS_PROPERTIES, ELEMENTS } from '@wix/react-component-schema'
|
|
11
12
|
import { camelCase } from 'case-anything'
|
|
@@ -21,10 +22,32 @@ import type {
|
|
|
21
22
|
import { getDefaultDisplayForTag, resolveDisplayValue } from '../information-extractors/react'
|
|
22
23
|
import { findPreferredSemanticClass } from '../utils/css-class'
|
|
23
24
|
import { resolveLonghand } from './css-longhand-resolver'
|
|
25
|
+
import { type CustomClassTrigger, buildCustomStatesBlock } from './custom-states-builder'
|
|
24
26
|
import { buildDataItem } from './data-item-builder'
|
|
25
27
|
import { buildStatesBlock } from './states-builder'
|
|
26
28
|
import { formatDisplayName } from './utils'
|
|
27
29
|
|
|
30
|
+
/**
|
|
31
|
+
* Merges native and custom state blocks. Native states take precedence on any
|
|
32
|
+
* key collision (e.g. an author wrapping `isDisabled` in `ElementState<>` does not
|
|
33
|
+
* shadow the inferred native `disabled` state).
|
|
34
|
+
*/
|
|
35
|
+
function mergeStates(native: States | undefined, custom: States | undefined): States | undefined {
|
|
36
|
+
if (!native && !custom) return undefined
|
|
37
|
+
const merged: States = { ...custom, ...native }
|
|
38
|
+
return Object.keys(merged).length > 0 ? merged : undefined
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/** Collects custom-class state triggers (modifiers with no paired native pseudo) across all CSS files. */
|
|
42
|
+
function collectCustomClassTriggers(component: ComponentInfoWithCss): CustomClassTrigger[] {
|
|
43
|
+
return component.css.flatMap((cssInfo) =>
|
|
44
|
+
cssInfo.api
|
|
45
|
+
.getStateClasses()
|
|
46
|
+
.filter((rule) => rule.pseudoClass === undefined)
|
|
47
|
+
.map((rule) => ({ ...rule, isCssModule: cssInfo.isCssModule })),
|
|
48
|
+
)
|
|
49
|
+
}
|
|
50
|
+
|
|
28
51
|
// Props to exclude from data items (same as in element-data.ts)
|
|
29
52
|
const EXCLUDED_PROPS = new Set(['id', 'className', 'elementProps', 'wix'])
|
|
30
53
|
|
|
@@ -43,8 +66,22 @@ function buildEditorElement(
|
|
|
43
66
|
const childElements = rootElement?.children ?? []
|
|
44
67
|
const rootCustomProps = rootElement ? (nearestCommonAncestorCustomProps.get(rootElement.traceId) ?? {}) : {}
|
|
45
68
|
|
|
69
|
+
const customClassTriggers = collectCustomClassTriggers(component)
|
|
70
|
+
|
|
71
|
+
// Native states need a semantic block name to synthesize `<block>--<state>`; custom
|
|
72
|
+
// class triggers don't (they carry the className verbatim and match the element
|
|
73
|
+
// directly), so they're built even when the element resolves no semantic class.
|
|
46
74
|
const rootSemanticClass = getSemanticBlockName(rootElement)
|
|
47
|
-
const rootStates = rootElement
|
|
75
|
+
const rootStates = rootElement
|
|
76
|
+
? mergeStates(
|
|
77
|
+
rootSemanticClass ? buildStatesBlock(rootElement, rootSemanticClass) : undefined,
|
|
78
|
+
buildCustomStatesBlock({
|
|
79
|
+
element: rootElement,
|
|
80
|
+
propTriggers: Object.values(component.props).filter((prop) => prop.isStateTrigger),
|
|
81
|
+
classTriggers: customClassTriggers,
|
|
82
|
+
}),
|
|
83
|
+
)
|
|
84
|
+
: undefined
|
|
48
85
|
|
|
49
86
|
return {
|
|
50
87
|
selector: buildSelector(rootElement),
|
|
@@ -53,6 +90,7 @@ function buildEditorElement(
|
|
|
53
90
|
elements: buildElements(
|
|
54
91
|
childElements,
|
|
55
92
|
nearestCommonAncestorCustomProps,
|
|
93
|
+
customClassTriggers,
|
|
56
94
|
component.innerElementProps,
|
|
57
95
|
component.propUsages,
|
|
58
96
|
),
|
|
@@ -100,6 +138,7 @@ function buildData(
|
|
|
100
138
|
function buildElements(
|
|
101
139
|
elements: ExtractedElement[],
|
|
102
140
|
nearestCommonAncestorCustomProps: Map<string, Record<string, CssCustomPropertyItem>>,
|
|
141
|
+
customClassTriggers: CustomClassTrigger[],
|
|
103
142
|
innerElementProps?: CoupledComponentInfo['innerElementProps'],
|
|
104
143
|
propUsages?: TrackingStores['propUsages'],
|
|
105
144
|
): Record<string, ElementItem> {
|
|
@@ -112,7 +151,12 @@ function buildElements(
|
|
|
112
151
|
const cssCustomProps = nearestCommonAncestorCustomProps.get(element.traceId) ?? {}
|
|
113
152
|
|
|
114
153
|
const semanticClass = getSemanticBlockName(element)
|
|
115
|
-
|
|
154
|
+
// Inner-element custom states come from custom-class triggers only; prop
|
|
155
|
+
// triggers (`ElementState<>`) are component-level and attach to the root.
|
|
156
|
+
const states = mergeStates(
|
|
157
|
+
semanticClass ? buildStatesBlock(element, semanticClass) : undefined,
|
|
158
|
+
buildCustomStatesBlock({ element, propTriggers: [], classTriggers: customClassTriggers }),
|
|
159
|
+
)
|
|
116
160
|
|
|
117
161
|
result[element.name] = {
|
|
118
162
|
elementType: ELEMENTS.ELEMENT_TYPE.inlineElement,
|
|
@@ -130,7 +174,13 @@ function buildElements(
|
|
|
130
174
|
// Recursively build nested elements
|
|
131
175
|
elements:
|
|
132
176
|
element.children.length > 0
|
|
133
|
-
? buildElements(
|
|
177
|
+
? buildElements(
|
|
178
|
+
element.children,
|
|
179
|
+
nearestCommonAncestorCustomProps,
|
|
180
|
+
customClassTriggers,
|
|
181
|
+
innerElementProps,
|
|
182
|
+
propUsages,
|
|
183
|
+
)
|
|
134
184
|
: undefined,
|
|
135
185
|
},
|
|
136
186
|
}
|
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
import { describe, expect, it } from 'vitest'
|
|
2
|
+
import { parseCss } from './parse'
|
|
3
|
+
|
|
4
|
+
describe('parseCss — getStateClasses', () => {
|
|
5
|
+
it('extracts a custom-class modifier with no paired pseudo', () => {
|
|
6
|
+
const stateClasses = parseCss(`
|
|
7
|
+
.root:global(.custom-states--featured) {
|
|
8
|
+
border: 2px solid orange;
|
|
9
|
+
}
|
|
10
|
+
`).getStateClasses()
|
|
11
|
+
|
|
12
|
+
expect(stateClasses).toEqual([
|
|
13
|
+
{ baseSelector: '.root', modifier: 'custom-states--featured', pseudoClass: undefined },
|
|
14
|
+
])
|
|
15
|
+
})
|
|
16
|
+
|
|
17
|
+
it('pairs a global modifier with a native pseudo on the same base in the same rule', () => {
|
|
18
|
+
const stateClasses = parseCss(`
|
|
19
|
+
.root:global(.toggle--hover),
|
|
20
|
+
.root:hover {
|
|
21
|
+
filter: brightness(1.05);
|
|
22
|
+
}
|
|
23
|
+
`).getStateClasses()
|
|
24
|
+
|
|
25
|
+
expect(stateClasses).toEqual([{ baseSelector: '.root', modifier: 'toggle--hover', pseudoClass: 'hover' }])
|
|
26
|
+
})
|
|
27
|
+
|
|
28
|
+
it('maps :focus-visible and :focus-within to the focus state', () => {
|
|
29
|
+
const stateClasses = parseCss(`
|
|
30
|
+
.root:global(.toggle--focus),
|
|
31
|
+
.root:focus-within {
|
|
32
|
+
outline: 2px solid;
|
|
33
|
+
}
|
|
34
|
+
`).getStateClasses()
|
|
35
|
+
|
|
36
|
+
expect(stateClasses).toEqual([{ baseSelector: '.root', modifier: 'toggle--focus', pseudoClass: 'focus' }])
|
|
37
|
+
})
|
|
38
|
+
|
|
39
|
+
it('keeps modifiers on distinct base elements separate', () => {
|
|
40
|
+
const stateClasses = parseCss(`
|
|
41
|
+
.root:global(.card--featured) { color: red; }
|
|
42
|
+
.label:global(.card__label--featured) { font-weight: 700; }
|
|
43
|
+
`).getStateClasses()
|
|
44
|
+
|
|
45
|
+
expect(stateClasses).toEqual([
|
|
46
|
+
{ baseSelector: '.root', modifier: 'card--featured', pseudoClass: undefined },
|
|
47
|
+
{ baseSelector: '.label', modifier: 'card__label--featured', pseudoClass: undefined },
|
|
48
|
+
])
|
|
49
|
+
})
|
|
50
|
+
|
|
51
|
+
it('treats a global base class with a global modifier as base + modifier (BEM extension)', () => {
|
|
52
|
+
const stateClasses = parseCss(`
|
|
53
|
+
:global(.card):global(.card--featured) {
|
|
54
|
+
border: 2px solid orange;
|
|
55
|
+
}
|
|
56
|
+
`).getStateClasses()
|
|
57
|
+
|
|
58
|
+
expect(stateClasses).toEqual([{ baseSelector: '.card', modifier: 'card--featured', pseudoClass: undefined }])
|
|
59
|
+
})
|
|
60
|
+
|
|
61
|
+
it('resolves a global base for an inner BEM element (`__`) carrying a modifier', () => {
|
|
62
|
+
const stateClasses = parseCss(`
|
|
63
|
+
:global(.card__label):global(.card__label--highlight) {
|
|
64
|
+
font-weight: 700;
|
|
65
|
+
}
|
|
66
|
+
`).getStateClasses()
|
|
67
|
+
|
|
68
|
+
expect(stateClasses).toEqual([
|
|
69
|
+
{ baseSelector: '.card__label', modifier: 'card__label--highlight', pseudoClass: undefined },
|
|
70
|
+
])
|
|
71
|
+
})
|
|
72
|
+
|
|
73
|
+
it('pairs a native pseudo with a modifier when the base element is global', () => {
|
|
74
|
+
const stateClasses = parseCss(`
|
|
75
|
+
:global(.card):global(.card--hover),
|
|
76
|
+
:global(.card):hover {
|
|
77
|
+
filter: brightness(1.05);
|
|
78
|
+
}
|
|
79
|
+
`).getStateClasses()
|
|
80
|
+
|
|
81
|
+
expect(stateClasses).toEqual([{ baseSelector: '.card', modifier: 'card--hover', pseudoClass: 'hover' }])
|
|
82
|
+
})
|
|
83
|
+
|
|
84
|
+
it('anchors a modifier to the compound it sits on, ignoring a descendant combinator', () => {
|
|
85
|
+
// SCSS `.header { &:global(.card__title--selected) { .label { … } } }` compiles to a
|
|
86
|
+
// descendant selector; the modifier belongs to `.header`, not `.header .label`.
|
|
87
|
+
const stateClasses = parseCss(`
|
|
88
|
+
.header:global(.card__title--selected) .label {
|
|
89
|
+
text-decoration: underline;
|
|
90
|
+
}
|
|
91
|
+
`).getStateClasses()
|
|
92
|
+
|
|
93
|
+
expect(stateClasses).toEqual([
|
|
94
|
+
{ baseSelector: '.header', modifier: 'card__title--selected', pseudoClass: undefined },
|
|
95
|
+
])
|
|
96
|
+
})
|
|
97
|
+
|
|
98
|
+
it('pairs only the same-named modifier with a pseudo when a base carries both native and custom modifiers', () => {
|
|
99
|
+
// One rule, descendant-anchored: `--hover` pairs with `:hover`; `--selected` stays custom.
|
|
100
|
+
const stateClasses = parseCss(`
|
|
101
|
+
.header:hover .label,
|
|
102
|
+
.header:global(.card__title--hover) .label,
|
|
103
|
+
.header:global(.card__title--selected) .label {
|
|
104
|
+
text-decoration: underline;
|
|
105
|
+
}
|
|
106
|
+
`).getStateClasses()
|
|
107
|
+
|
|
108
|
+
expect(stateClasses).toEqual([
|
|
109
|
+
{ baseSelector: '.header', modifier: 'card__title--hover', pseudoClass: 'hover' },
|
|
110
|
+
{ baseSelector: '.header', modifier: 'card__title--selected', pseudoClass: undefined },
|
|
111
|
+
])
|
|
112
|
+
})
|
|
113
|
+
|
|
114
|
+
it('ignores rules with no global modifier', () => {
|
|
115
|
+
const stateClasses = parseCss(`
|
|
116
|
+
.root:hover { cursor: pointer; }
|
|
117
|
+
.root { color: black; }
|
|
118
|
+
`).getStateClasses()
|
|
119
|
+
|
|
120
|
+
expect(stateClasses).toEqual([])
|
|
121
|
+
})
|
|
122
|
+
})
|