@wix/zero-config-implementation 1.65.0 → 1.67.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-DbvAWhJF.js → index-BfHROU60.js} +1 -1
- package/dist/{index-BLr9vmnH.js → index-Ps7eUbv5.js} +13440 -13222
- package/dist/index.d.ts +28 -0
- package/dist/index.js +1 -1
- package/package.json +2 -2
- package/src/converters/custom-states-builder.test.ts +72 -0
- package/src/converters/custom-states-builder.ts +75 -0
- package/src/converters/states-builder.ts +58 -0
- package/src/converters/to-editor-component.ts +59 -1
- 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/react/extractors/index.ts +3 -0
- package/src/information-extractors/react/extractors/prop-tracker.ts +8 -0
- package/src/information-extractors/react/extractors/state-markers.test.ts +143 -0
- package/src/information-extractors/react/extractors/state-markers.ts +142 -0
- package/src/information-extractors/react/index.ts +2 -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
|
*/
|
|
@@ -2369,6 +2377,8 @@ export declare interface PropTrackerData {
|
|
|
2369
2377
|
role?: string;
|
|
2370
2378
|
boundProps: string[];
|
|
2371
2379
|
concatenatedAttrs: Map<string, string>;
|
|
2380
|
+
/** on*-handler prop names whose value is a function on this element (e.g. ['onClick', 'onChange']). */
|
|
2381
|
+
eventHandlers: string[];
|
|
2372
2382
|
}
|
|
2373
2383
|
|
|
2374
2384
|
export declare interface PropTrackerExtractorState {
|
|
@@ -2642,6 +2652,24 @@ CauseArg extends Cause,
|
|
|
2642
2652
|
> = MainInstanceOptions<AggregateErrorsArg, CauseArg> &
|
|
2643
2653
|
PluginsOptions<PluginsArg, ChildProps>
|
|
2644
2654
|
|
|
2655
|
+
/**
|
|
2656
|
+
* A `:global(.modifier)` design-state class found on a base element selector.
|
|
2657
|
+
* Produced by walking each CSS rule and pairing the global modifier with any
|
|
2658
|
+
* native pseudo-class present on the same base within that rule.
|
|
2659
|
+
*/
|
|
2660
|
+
declare interface StateClassRule {
|
|
2661
|
+
/** DOM-matchable base selector (e.g. `.root`), used to map the modifier to an element. */
|
|
2662
|
+
baseSelector: string;
|
|
2663
|
+
/** Global modifier class name from `:global(.X)`, without the leading dot. */
|
|
2664
|
+
modifier: string;
|
|
2665
|
+
/**
|
|
2666
|
+
* Native pseudo-class paired with this modifier on the same base within the same
|
|
2667
|
+
* rule. When present, the modifier is the editor-trigger class for a native state;
|
|
2668
|
+
* when absent, the modifier denotes a custom (non-native) design state.
|
|
2669
|
+
*/
|
|
2670
|
+
pseudoClass?: NativePseudoClass;
|
|
2671
|
+
}
|
|
2672
|
+
|
|
2645
2673
|
/**
|
|
2646
2674
|
* Unbound static method of a plugin
|
|
2647
2675
|
*/
|
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-Ps7eUbv5.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.67.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": "98b2efba108de51cc5bc3678f7b32253ba1a26e9b875964b6a8e725e"
|
|
85
85
|
}
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
import { describe, expect, it } from 'vitest'
|
|
2
|
+
import type { ExtractedElement } from '../information-extractors/react'
|
|
3
|
+
import { type CustomClassTrigger, buildCustomStatesBlock } from './custom-states-builder'
|
|
4
|
+
|
|
5
|
+
function elementWithClass(classAttribute: string): ExtractedElement {
|
|
6
|
+
return { attributes: { class: classAttribute } } as unknown as ExtractedElement
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
function classTrigger(baseSelector: string, modifier: string, isCssModule = false): CustomClassTrigger {
|
|
10
|
+
return { baseSelector, modifier, isCssModule }
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
describe('buildCustomStatesBlock', () => {
|
|
14
|
+
it('emits a custom-class state with className only (no props, no pseudoClass)', () => {
|
|
15
|
+
const states = buildCustomStatesBlock({
|
|
16
|
+
element: elementWithClass('custom-states'),
|
|
17
|
+
classTriggers: [classTrigger('.custom-states', 'custom-states--featured')],
|
|
18
|
+
})
|
|
19
|
+
|
|
20
|
+
expect(states).toEqual({ featured: { displayName: 'Featured', className: 'custom-states--featured' } })
|
|
21
|
+
})
|
|
22
|
+
|
|
23
|
+
it('derives a multi-word state name from the modifier and capital-cases the displayName', () => {
|
|
24
|
+
const states = buildCustomStatesBlock({
|
|
25
|
+
element: elementWithClass('widget'),
|
|
26
|
+
classTriggers: [classTrigger('.widget', 'widget--in-progress')],
|
|
27
|
+
})
|
|
28
|
+
|
|
29
|
+
expect(states?.['in-progress']).toEqual({
|
|
30
|
+
displayName: 'In Progress',
|
|
31
|
+
className: 'widget--in-progress',
|
|
32
|
+
})
|
|
33
|
+
})
|
|
34
|
+
|
|
35
|
+
it('does not apply a block-base class trigger to a BEM child element (plain CSS, exact match)', () => {
|
|
36
|
+
const states = buildCustomStatesBlock({
|
|
37
|
+
element: elementWithClass('custom-states__label'),
|
|
38
|
+
classTriggers: [classTrigger('.custom-states', 'custom-states--featured')],
|
|
39
|
+
})
|
|
40
|
+
|
|
41
|
+
expect(states).toBeUndefined()
|
|
42
|
+
})
|
|
43
|
+
|
|
44
|
+
it('matches a base class rendered as a double-underscore CSS-module hash (`root__HASH`)', () => {
|
|
45
|
+
// Vite emits `[name]__[hash]` for `styles.root`; the hash carries uppercase, so it is the
|
|
46
|
+
// base element — not a BEM child. (Regression: this used to be dropped as a child.)
|
|
47
|
+
const states = buildCustomStatesBlock({
|
|
48
|
+
element: elementWithClass('root__IWjkn wixui-test-comp-card test-comp-card'),
|
|
49
|
+
classTriggers: [classTrigger('.root', 'test-comp-card--featured', true)],
|
|
50
|
+
})
|
|
51
|
+
|
|
52
|
+
expect(states).toEqual({ featured: { displayName: 'Featured', className: 'test-comp-card--featured' } })
|
|
53
|
+
})
|
|
54
|
+
|
|
55
|
+
it('still excludes a genuine BEM child (`root__label`) from a block-base trigger in a module', () => {
|
|
56
|
+
const states = buildCustomStatesBlock({
|
|
57
|
+
element: elementWithClass('root__label'),
|
|
58
|
+
classTriggers: [classTrigger('.root', 'test-comp-card--featured', true)],
|
|
59
|
+
})
|
|
60
|
+
|
|
61
|
+
expect(states).toBeUndefined()
|
|
62
|
+
})
|
|
63
|
+
|
|
64
|
+
it('returns undefined when no class trigger targets the element', () => {
|
|
65
|
+
const states = buildCustomStatesBlock({
|
|
66
|
+
element: elementWithClass('custom-states'),
|
|
67
|
+
classTriggers: [],
|
|
68
|
+
})
|
|
69
|
+
|
|
70
|
+
expect(states).toBeUndefined()
|
|
71
|
+
})
|
|
72
|
+
})
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Builds the custom (non-native) `states` entries for an element.
|
|
3
|
+
*
|
|
4
|
+
* The custom-class trigger feeds this: a `:global(.modifier)` class on the element
|
|
5
|
+
* with no paired native pseudo (CSS extractor `getStateClasses`). It emits the
|
|
6
|
+
* modifier as the state's editor-trigger `className`.
|
|
7
|
+
*
|
|
8
|
+
* Native states are built separately and take precedence on any key collision.
|
|
9
|
+
*
|
|
10
|
+
* Note: the prop trigger (`State<>` markers → `props` overrides) is intentionally
|
|
11
|
+
* not handled here yet — it is deferred to a follow-up PR, gated on the upstream
|
|
12
|
+
* marker-type agreement.
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
import type { States } from '@wix/react-component-schema'
|
|
16
|
+
import type { StateClassRule } from '../information-extractors/css/types'
|
|
17
|
+
import type { ExtractedElement } from '../information-extractors/react'
|
|
18
|
+
import { isGlobalSemanticClass, stateNameFromModifier } from '../utils/css-class'
|
|
19
|
+
import { formatDisplayName } from './utils'
|
|
20
|
+
|
|
21
|
+
/** A custom-class trigger tagged with whether its source file is a CSS module. */
|
|
22
|
+
export type CustomClassTrigger = StateClassRule & { isCssModule: boolean }
|
|
23
|
+
|
|
24
|
+
interface BuildCustomStatesParams {
|
|
25
|
+
element: ExtractedElement
|
|
26
|
+
/** Custom-class triggers (modifiers with no paired native pseudo) across all CSS files. */
|
|
27
|
+
classTriggers: CustomClassTrigger[]
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export function buildCustomStatesBlock({ element, classTriggers }: BuildCustomStatesParams): States | undefined {
|
|
31
|
+
const states: States = {}
|
|
32
|
+
|
|
33
|
+
// Custom-class triggers that target this element.
|
|
34
|
+
for (const rule of classTriggers) {
|
|
35
|
+
if (!elementMatchesBase(element, rule.baseSelector, rule.isCssModule)) continue
|
|
36
|
+
const stateName = stateNameFromModifier(rule.modifier)
|
|
37
|
+
states[stateName] = {
|
|
38
|
+
displayName: formatDisplayName(stateName),
|
|
39
|
+
className: rule.modifier,
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
return Object.keys(states).length > 0 ? states : undefined
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* Checks whether an element's rendered class list matches a simple single-class
|
|
48
|
+
* base selector (e.g. `.root`, `.custom-states`).
|
|
49
|
+
*
|
|
50
|
+
* Exact class match always counts. For CSS-module files the Vite hash patterns also
|
|
51
|
+
* count: `localName_HASH`, `_localName_HASH`, and the double-underscore `localName__HASH`
|
|
52
|
+
* form. The catch is that `localName__HASH` is syntactically identical to a BEM child
|
|
53
|
+
* class (`localName__child`) — which denotes a *different* element and must NOT match the
|
|
54
|
+
* block's base selector. We disambiguate by shape: a real BEM child is an all-lowercase
|
|
55
|
+
* semantic name (`isGlobalSemanticClass`), whereas a module hash carries uppercase / other
|
|
56
|
+
* non-kebab characters. So only genuine BEM children are excluded; hashed base classes match.
|
|
57
|
+
*/
|
|
58
|
+
function elementMatchesBase(element: ExtractedElement, baseSelector: string, isCssModule: boolean): boolean {
|
|
59
|
+
const match = baseSelector.match(/^\.([\w-]+)$/)
|
|
60
|
+
if (!match) return false
|
|
61
|
+
|
|
62
|
+
const localName = match[1]
|
|
63
|
+
const classList = element.attributes.class?.split(/\s+/).filter(Boolean) ?? []
|
|
64
|
+
return classList.some((className) => {
|
|
65
|
+
if (className === localName) return true
|
|
66
|
+
if (!isCssModule) return false
|
|
67
|
+
// Genuine BEM child (`localName__<lowercase-element>`) → a different element, exclude it.
|
|
68
|
+
// A double-underscore module hash (`localName__<hash>`) is NOT semantic, so it falls through.
|
|
69
|
+
const isBemChild =
|
|
70
|
+
(className.startsWith(`${localName}__`) || className.startsWith(`_${localName}__`)) &&
|
|
71
|
+
isGlobalSemanticClass(className.replace(/^_/, ''))
|
|
72
|
+
if (isBemChild) return false
|
|
73
|
+
return className.startsWith(`${localName}_`) || className.startsWith(`_${localName}_`)
|
|
74
|
+
})
|
|
75
|
+
}
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Builds the `states` block for the root EditorElement and inline elements.
|
|
3
|
+
*
|
|
4
|
+
* Native states only (hover/focus/disabled/invalid). Signals come from the
|
|
5
|
+
* React extractor (tag, role, event handlers). The CSS extractor is not
|
|
6
|
+
* consulted.
|
|
7
|
+
*
|
|
8
|
+
* className follows BEM by appending `--<state>` to the element's own
|
|
9
|
+
* semantic class. Component authors are expected to write inner elements
|
|
10
|
+
* with full BEM class names (e.g. `toggle__label`), so the resulting state
|
|
11
|
+
* className is `<semanticClass>--<state>`:
|
|
12
|
+
* - root `toggle` → `toggle--hover`
|
|
13
|
+
* - inner `toggle__label` → `toggle__label--hover`
|
|
14
|
+
*
|
|
15
|
+
* `props` is never emitted on native-state entries — that field is reserved
|
|
16
|
+
* for functional (non-native) states.
|
|
17
|
+
*/
|
|
18
|
+
|
|
19
|
+
import type { States } from '@wix/react-component-schema'
|
|
20
|
+
import { NATIVE_STATE_TYPE } from '@wix/react-component-schema'
|
|
21
|
+
import {
|
|
22
|
+
type ExtractedElement,
|
|
23
|
+
type SupportedNativeStates,
|
|
24
|
+
inferSupportedNativeStates,
|
|
25
|
+
} from '../information-extractors/react'
|
|
26
|
+
|
|
27
|
+
const STATE_DISPLAY_NAMES = {
|
|
28
|
+
hover: 'Hover',
|
|
29
|
+
focus: 'Focus',
|
|
30
|
+
disabled: 'Disabled',
|
|
31
|
+
invalid: 'Invalid',
|
|
32
|
+
} as const
|
|
33
|
+
|
|
34
|
+
type NativeStateName = keyof SupportedNativeStates
|
|
35
|
+
|
|
36
|
+
const PSEUDO_CLASS_BY_STATE: Record<NativeStateName, NATIVE_STATE_TYPE> = {
|
|
37
|
+
hover: NATIVE_STATE_TYPE.hover,
|
|
38
|
+
focus: NATIVE_STATE_TYPE.focus,
|
|
39
|
+
disabled: NATIVE_STATE_TYPE.disabled,
|
|
40
|
+
invalid: NATIVE_STATE_TYPE.invalid,
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
export function buildStatesBlock(element: ExtractedElement, semanticClass: string): States | undefined {
|
|
44
|
+
const supported = inferSupportedNativeStates(element)
|
|
45
|
+
const states: States = {}
|
|
46
|
+
|
|
47
|
+
for (const stateName of Object.keys(supported) as NativeStateName[]) {
|
|
48
|
+
if (!supported[stateName]) continue
|
|
49
|
+
|
|
50
|
+
states[stateName] = {
|
|
51
|
+
displayName: STATE_DISPLAY_NAMES[stateName],
|
|
52
|
+
className: `${semanticClass}--${stateName}`,
|
|
53
|
+
pseudoClass: PSEUDO_CLASS_BY_STATE[stateName],
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
return Object.keys(states).length > 0 ? states : undefined
|
|
58
|
+
}
|
|
@@ -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,9 +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'
|
|
27
|
+
import { buildStatesBlock } from './states-builder'
|
|
25
28
|
import { formatDisplayName } from './utils'
|
|
26
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 `State<>` 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
|
+
|
|
27
51
|
// Props to exclude from data items (same as in element-data.ts)
|
|
28
52
|
const EXCLUDED_PROPS = new Set(['id', 'className', 'elementProps', 'wix'])
|
|
29
53
|
|
|
@@ -42,6 +66,19 @@ function buildEditorElement(
|
|
|
42
66
|
const childElements = rootElement?.children ?? []
|
|
43
67
|
const rootCustomProps = rootElement ? (nearestCommonAncestorCustomProps.get(rootElement.traceId) ?? {}) : {}
|
|
44
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.
|
|
74
|
+
const rootSemanticClass = getSemanticBlockName(rootElement)
|
|
75
|
+
const rootStates = rootElement
|
|
76
|
+
? mergeStates(
|
|
77
|
+
rootSemanticClass ? buildStatesBlock(rootElement, rootSemanticClass) : undefined,
|
|
78
|
+
buildCustomStatesBlock({ element: rootElement, classTriggers: customClassTriggers }),
|
|
79
|
+
)
|
|
80
|
+
: undefined
|
|
81
|
+
|
|
45
82
|
return {
|
|
46
83
|
selector: buildSelector(rootElement),
|
|
47
84
|
displayName: formatDisplayName(component.componentName),
|
|
@@ -49,14 +86,21 @@ function buildEditorElement(
|
|
|
49
86
|
elements: buildElements(
|
|
50
87
|
childElements,
|
|
51
88
|
nearestCommonAncestorCustomProps,
|
|
89
|
+
customClassTriggers,
|
|
52
90
|
component.innerElementProps,
|
|
53
91
|
component.propUsages,
|
|
54
92
|
),
|
|
55
93
|
cssProperties: buildCssProperties(rootElement),
|
|
56
94
|
cssCustomProperties: rootCustomProps,
|
|
95
|
+
...(rootStates && { states: rootStates }),
|
|
57
96
|
}
|
|
58
97
|
}
|
|
59
98
|
|
|
99
|
+
function getSemanticBlockName(element: ExtractedElement | undefined): string | undefined {
|
|
100
|
+
if (!element?.attributes.class) return undefined
|
|
101
|
+
return findPreferredSemanticClass(element.attributes.class.split(' '))
|
|
102
|
+
}
|
|
103
|
+
|
|
60
104
|
function buildSelector(rootElement?: ExtractedElement): string {
|
|
61
105
|
if (rootElement?.attributes.class) {
|
|
62
106
|
const semanticClass = findPreferredSemanticClass(rootElement.attributes.class.split(' '))
|
|
@@ -90,6 +134,7 @@ function buildData(
|
|
|
90
134
|
function buildElements(
|
|
91
135
|
elements: ExtractedElement[],
|
|
92
136
|
nearestCommonAncestorCustomProps: Map<string, Record<string, CssCustomPropertyItem>>,
|
|
137
|
+
customClassTriggers: CustomClassTrigger[],
|
|
93
138
|
innerElementProps?: CoupledComponentInfo['innerElementProps'],
|
|
94
139
|
propUsages?: TrackingStores['propUsages'],
|
|
95
140
|
): Record<string, ElementItem> {
|
|
@@ -101,6 +146,12 @@ function buildElements(
|
|
|
101
146
|
const cssProps = buildCssProperties(element)
|
|
102
147
|
const cssCustomProps = nearestCommonAncestorCustomProps.get(element.traceId) ?? {}
|
|
103
148
|
|
|
149
|
+
const semanticClass = getSemanticBlockName(element)
|
|
150
|
+
const states = mergeStates(
|
|
151
|
+
semanticClass ? buildStatesBlock(element, semanticClass) : undefined,
|
|
152
|
+
buildCustomStatesBlock({ element, classTriggers: customClassTriggers }),
|
|
153
|
+
)
|
|
154
|
+
|
|
104
155
|
result[element.name] = {
|
|
105
156
|
elementType: ELEMENTS.ELEMENT_TYPE.inlineElement,
|
|
106
157
|
inlineElement: {
|
|
@@ -113,10 +164,17 @@ function buildElements(
|
|
|
113
164
|
...(Object.keys(cssProps).length > 0 && { cssProperties: cssProps }),
|
|
114
165
|
// CSS custom properties placed on the Nearest Common Ancestor of all elements that use each variable
|
|
115
166
|
...(Object.keys(cssCustomProps).length > 0 && { cssCustomProperties: cssCustomProps }),
|
|
167
|
+
...(states && { states }),
|
|
116
168
|
// Recursively build nested elements
|
|
117
169
|
elements:
|
|
118
170
|
element.children.length > 0
|
|
119
|
-
? buildElements(
|
|
171
|
+
? buildElements(
|
|
172
|
+
element.children,
|
|
173
|
+
nearestCommonAncestorCustomProps,
|
|
174
|
+
customClassTriggers,
|
|
175
|
+
innerElementProps,
|
|
176
|
+
propUsages,
|
|
177
|
+
)
|
|
120
178
|
: undefined,
|
|
121
179
|
},
|
|
122
180
|
}
|
|
@@ -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
|
+
})
|